Commit Graph

6641 Commits

Author SHA1 Message Date
Kunthawat Greethong
832a7fdae0 [verified] Remove Chatwoot Hub sync paths 2026-08-15 13:23:03 +07:00
Kunthawat Greethong
8ebb320bd4 [verified] Add privacy and branding audit harness 2026-08-15 13:10:14 +07:00
Sivin Varghese
9a73c1473f feat: add icons to macro and automation dropdowns (#15447) 2026-08-14 08:45:58 +05:30
Shivam Mishra
59942e6890 fix: inbox contact sidebar overflow (#15470)
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.
2026-08-13 19:50:03 +05:30
Vishnu Narayanan
fefa0a6966 perf: add composite index for account-scoped conversation created_at sort (#15360)
## Description

Adds a composite index `(account_id, status, created_at)` on
`conversations`.

Sorting an account's conversation list by `created_at` ("newest first")
across all inboxes has no supporting index today, so Postgres falls back
to a backward scan of the global `index_conversations_on_created_at`
filtered by `account_id`. On large databases the planner estimates a
very deep scan and the query can exceed the statement timeout. The
composite index lets the ordered limit be served directly from the
`account_id`/`status` prefix. The default `last_activity_at` sort and
inbox-filtered views are unaffected (already indexed).

The index is created with `algorithm: :concurrently` (no table lock). On
very large installations, create it out-of-band concurrently before
deploying so the migration doesn't run a long build.

Fixes https://linear.app/chatwoot/issue/CW-7888

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
2026-08-13 18:34:12 +05:30
Shivam Mishra
40f5381da6 feat: add Captain outcome reporting builders (#15425)
This PR adds outcome-based reporting builders for the redesigned Captain
overview, resolution flow, and resolution trend. These builders are not
wired to controllers or the frontend yet, so the existing Captain
metrics remain unchanged.

## AssistantOverviewStatsBuilder

Builds current and previous reporting-window metrics, including the
comparison trend, for the overview and CSAT cards.

| Statistic | Description |
| --- | --- |
| Conversations handled | Counts outcome episodes in which Captain
replied or performed a non-usage-limit handoff. |
| Auto-resolution rate | Shows autonomous resolutions as a percentage of
conversations handled. |
| Autonomous resolutions | Counts episodes resolved by Captain without a
handoff or an earlier human reply. |
| Handoff rate | Shows involved handoffs as a percentage of
conversations handled. |
| Handoff count | Counts involved handoffs while excluding demand
blocked by usage limits. |
| Hours saved | Estimates displaced agent effort from Captain's public
replies at two minutes per reply. |
| Reopen rate | Shows the share of autonomous resolutions followed by
another episode. |
| Conversation depth | Shows the average number of public Captain
replies per replied-to conversation. |
| Durable resolution rate | Shows autonomous resolutions that remained
closed for at least seven days among resolutions old enough to assess. |
| Autonomous CSAT score | Averages CSAT ratings from conversations
resolved autonomously by Captain. |
| Assisted CSAT score | Averages CSAT ratings from resolved
conversations where Captain participated alongside a human. |
| Human-only CSAT score | Averages account CSAT from conversations where
Captain never participated. |
| Median resolution time | Reports the median elapsed seconds from
demand start to resolution for handled episodes. |

## AssistantResolutionFlowBuilder

Builds the current-window Sankey data and a matching handoff-reason
distribution from the same outcome cohort.

| Statistic | Description |
| --- | --- |
| Conversations handled | Provides the Sankey entry count for episodes
where Captain participated. |
| Resolved by Captain | Counts handled episodes resolved autonomously by
Captain. |
| Handed off | Counts handled episodes transferred to a human for a
non-usage-limit reason. |
| Closed with team | Counts handled episodes outside the
autonomous-resolution and handoff branches. |
| Reopened within seven days | Counts Captain resolutions followed by a
new episode before the seven-day durability boundary. |
| Stayed closed | Counts Captain resolutions with no reopen inside seven
days. |
| Handoff reason nodes | Shows the two largest handoff categories and
combines the remainder as other reasons. |
| Handoff distribution | Returns every involved handoff category with
its count and percentage, including unclassified handoffs. |

## AssistantResolutionTrendStatsBuilder

Builds a zero-filled, timezone-aware resolution series in one outcome
query, using daily buckets for windows of 15 days or less and weekly
buckets for longer windows.

| Statistic | Description |
| --- | --- |
| Granularity | Identifies whether the response contains daily or weekly
buckets. |
| Bucket range | Returns the start and end date represented by each
bucket. |
| Conversations handled | Counts Captain-involved outcome episodes whose
demand started in each bucket. |
| Resolved by Captain | Counts autonomously resolved outcome episodes
whose demand started in each bucket. |
2026-08-13 18:29:22 +05:30
Vishnu Narayanan
a929955cc3 fix: escape formula characters in contact CSV export (#15334)
## Description

Contact export builds its CSV with the standard library `CSV`, which
writes user-supplied values (name, email, phone, custom attributes)
verbatim. When a cell begins with a formula character (`=`, `+`, `-`,
`@`, and tab/CR), spreadsheet applications interpret it as a formula on
open. This switches the export to `CSVSafe` (the `csv-safe` gem already
used by the v2 report exports), which prefixes such fields so they are
treated as text.

No new dependency, no behavioural change beyond neutralising
formula-leading cells. Note: phone numbers stored with a leading `+` are
now prefixed with a single quote in the exported file, consistent with
how the report exports already behave.

Ref https://linear.app/chatwoot/issue/CW-7473

## Type of change

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

## How Has This Been Tested?

`bundle exec rspec spec/jobs/account/contacts_export_job_spec.rb` —
added a case asserting a formula-leading contact value is neutralised on
export; existing cases updated for the phone-number prefix.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [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
2026-08-13 18:09:38 +05:30
Aakash Bakhle
8864f80ab7 feat(captain): show document conversation usage (#15140)
## 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)
2026-08-13 17:39:49 +05:30
Sony Mathew
b3a1dbca81 fix: oversized message cursors (#15400)
Message pagination now constrains client-provided cursors to the
PostgreSQL integer range used by `messages.id`, preventing oversized
values from raising database errors while preserving before/after
pagination semantics.

## Closes

-
[CW-7922](https://linear.app/chatwoot/issue/CW-7922/harden-backend-paths-causing-production-sentry-errors)
- [Sentry 7663397140](https://chatwoot-p3.sentry.io/issues/7663397140/)
- [Sentry 7663397546](https://chatwoot-p3.sentry.io/issues/7663397546/)
- [Sentry 7663464772](https://chatwoot-p3.sentry.io/issues/7663464772/)

## How to reproduce

Request conversation messages with an extremely large `before` or
`after` cursor, such as `4611686018427387903`. PostgreSQL previously
rejected the value as outside the range for the integer `messages.id`
column.

## What changed

- Normalize message cursors before they reach the query.
- Clamp them to the valid signed 32-bit integer ID range.
- Cover oversized `before` and `after` cursors with finder specs.
2026-08-13 17:34:05 +05:30
Sony Mathew
dd203438d3 fix: reject malformed filter payloads (#15398)
Contact and conversation filters now reject malformed numeric/date
custom-attribute values and dangling query operators during validation,
returning the existing invalid-filter response instead of failing later
while building SQL.

## Closes

-
[CW-7922](https://linear.app/chatwoot/issue/CW-7922/harden-backend-paths-causing-production-sentry-errors)
- [Sentry 7663920842](https://chatwoot-p3.sentry.io/issues/7663920842/)
- [Sentry 7663378104](https://chatwoot-p3.sentry.io/issues/7663378104/)

## How to reproduce

Submit a custom numeric attribute filter with an empty value, or end a
filter payload with a nonblank `query_operator`. These inputs previously
reached coercion/query construction and raised production exceptions.

## What changed

- Validate numeric and date custom-attribute values before query
construction.
- Preserve presence operators, which do not require a comparison value.
- Reject a query operator on the final filter condition.
- Add contact and conversation service regression coverage.
2026-08-13 17:04:11 +05:30
Sony Mathew
8a778f4f5b fix: handle empty round robin queues (#15399)
Round-robin queue resets now leave the Redis queue empty when an inbox
has no members, avoiding an invalid sorted-set write while keeping
normal queue population unchanged.

## Closes

-
[CW-7922](https://linear.app/chatwoot/issue/CW-7922/harden-backend-paths-causing-production-sentry-errors)
- [Sentry 7663380084](https://chatwoot-p3.sentry.io/issues/7663380084/)

## How to reproduce

Run round-robin assignment for an inbox with no inbox members. Resetting
the queue previously attempted a Redis sorted-set write without any
member/score pairs.

## What changed

- Clear the existing queue as before.
- Skip queue population when there are no eligible user IDs.
- Add service coverage for an inbox with no members.
2026-08-13 17:03:36 +05:30
Vishnu Narayanan
ba1973ea40 fix: escape formula characters in CSAT CSV export (#15335)
## Description

The CSAT survey response CSV export builds rows with the standard
library `CSV`, writing values verbatim, including the feedback message,
which is free text submitted by end users. When a cell begins with a
formula character (`=`, `+`, `-`, `@`, and tab/CR), spreadsheet
applications interpret it as a formula on open. This switches the three
`CSV.generate_line` calls in the export template to
`CSVSafe.generate_line` (the `csv-safe` gem already used by the v2
report exports), which prefixes such fields so they are treated as text.

No new dependency, no behavioural change beyond neutralising
formula-leading cells.

Ref https://linear.app/chatwoot/issue/CW-7473

## Type of change

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

## How Has This Been Tested?

`bundle exec rspec
spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb`
— added a case asserting a formula-leading feedback value is neutralised
in the downloaded CSV.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [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
2026-08-13 16:23:05 +05:30
Aakash Bakhle
13de83d1dc refactor(captain): route conversation completion by feature (#15317)
Conversation completion evaluations now use a dedicated internal LLM
feature with GPT 4.1 as the default. The internal route keeps the
completion model separate from the installation wide Captain model
override and from the assistant route, which can use GPT 5.2 for Captain
V2 accounts. Evaluations continue to use the installation API key and do
not consume Captain response credits.

## What changed

Added an internal `conversation_completion` feature to the LLM model
config and excluded internal features from account preferences, the
Captain settings API, and Super Admin model overrides.

Updated `Captain::ConversationCompletionService` to resolve its model
through `Llm::FeatureRouter`.

Added focused service and request coverage for model routing and
settings visibility.
2026-08-13 13:54:35 +05:30
Aakash Bakhle
eff7ce2898 fix: consolidate customer message bursts before lookup (#15263)
Captain now treats consecutive customer messages as one request before
it asks a question, searches the knowledge base, or suggests a solution.
It uses the full message burst to identify the customer's goal and
current state. It checks conflicting details before relying on them.

## How to test

1. Send several customer messages in a row that describe one issue. Add
a short correction in a later message.
2. Confirm Captain uses all the messages before it asks a question,
searches the knowledge base, or suggests a solution.
3. Send another message with a detail that conflicts with information
Captain already checked. Confirm Captain checks the detail again before
relying on it.
2026-08-13 13:23:43 +05:30
Aakash Bakhle
b24a970795 fix: allow multi-digit values in duration inputs (#15429)
## 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.
2026-08-13 12:52:13 +05:30
Sivin Varghese
45878923b6 feat: run macros from the command bar (#15422) 2026-08-13 12:47:23 +05:30
Sivin Varghese
416ac8660f fix: fit the reply editor to Copilot suggestions (#15428)
# 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
2026-08-13 12:39:56 +05:30
Sivin Varghese
13dfe1a6c6 feat: run macros from the reply editor with # (#15416) 2026-08-13 12:23:35 +05:30
Manoj
1d905fb019 fix: anchor Label title validation with \z instead of \Z (#15443)
## Description

This isn't tied to an open issue — I found it by extrapolating from the
bug class fixed in #15415 ("fix: anchor contact phone number
validation"), which fixed a `Contact#phone_number` format validation
that was missing a leading `\A` anchor. That made me audit every
hand-written regex-based validation in the codebase for the same class
of anchoring mistake (`format: { with: ... }` validators, plus
`match?`/`=~` calls used for validation-style checks, across `app/`,
`enterprise/`, and `lib/`).

Everything else was already correctly anchored. One real instance of the
*sibling* mistake remains:

`RegexHelper::UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE` (used only by
`Label#title`'s format validation) is
`/\A[\p{L}\p{N}]+[\p{L}\p{N}_-]+\Z/` — note `\Z` (capital), not `\z`.
Unlike `\z`, `\Z` also matches just before a single trailing `"\n"` at
the end of the string. The surrounding comment documents the intended
character set (unicode letters/numbers/underscore/hyphen, not starting
with `_`/`-`) and says nothing about tolerating a trailing newline, so
this reads as an unintentional choice of anchor rather than a deliberate
one.

Concretely: `Label.new(title: "hello_world\n").valid?` returns `true` on
current `develop` and persists a title with a literal trailing newline,
because `\Z` lets the `\n` slip through. `Label` only lowercases the
title before validating (no `strip`), so nothing else catches this.

This is a narrower/lower-severity variant of the #15415 bug (it only
ever admits one specific trailing character, not an arbitrary
prefix/suffix), but it's the same underlying mistake, independently
verified against current source, not just pattern-matched from the diff.

## What changed

- `lib/regex_helper.rb`: `UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE`
now ends in `\z` instead of `\Z`, with a comment explaining why.

## Type of change

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

## How Has This Been Tested?

Ran against a real local Rails env (Ruby 3.4.4, PostgreSQL 16, Redis):

`bundle exec rspec spec/models/label_spec.rb`

Added a regression test asserting `Label.new(title: "hello_world\n")` is
invalid. Confirmed it fails against the pre-fix `\Z` regex and passes
after switching to `\z`. All existing `label_spec.rb` examples
(including the existing "foreign characters", "special characters", and
"uppercase" title-validation cases) continue to pass unchanged.

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes

🤖 This fix was authored by an AI coding agent (Claude) working on behalf
of Mithtech, an ERPNext/Frappe/Medusa.js implementation studio, as part
of a deliberate effort to build a track record of verified upstream
open-source contributions. Flagging this transparently per common
courtesy — happy to answer any questions about the change, including how
it was found (auditing for the same regex-anchor mistake class as
#15415).

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 12:05:06 +05:30
Sivin Varghese
2ed68cf207 feat: cache canned responses in the browser (#15401) 2026-08-13 11:54:32 +05:30
Sivin Varghese
590e17dfb7 chore: remove the legacy notifications page (#15426) 2026-08-13 11:49:44 +05:30
Muhsin Keloth
046c6c754f fix(messages): resend failed messages through provider (#15432)
Failed messages on external provider-delivery channels can appear sent
after an agent selects Retry even though the retry never reaches the
provider. This change clears the stale provider delivery ID before
retrying the existing message, allowing the channel service to create a
fresh provider attempt and store its new ID.

API and WebWidget messages are excluded because their `source_id` values
may be client-supplied correlation identifiers and their retry jobs do
not assign replacement provider IDs. Concurrent or stale retry requests
are also guarded so only the request that successfully claims a failed
message can enqueue delivery.

Fixes https://github.com/chatwoot/chatwoot/issues/14120

Related:
https://linear.app/chatwoot/issue/CW-6896/retrying-a-failed-whatsapp-message-silently-succeeds-locally-but-never

### Things to know

- Applies to external provider-delivery channels, including direct
WhatsApp and Twilio WhatsApp.
- Preserves `source_id` for `Channel::Api` and `Channel::WebWidget`.
- Reuses the existing Chatwoot message record while allowing the
provider to return a new message ID.
- Logs the cleared provider ID and Chatwoot message ID when a stale
provider ID is removed.

### How to reproduce

1. Send a message that the provider accepts and later marks as failed
through a status callback.
2. Select Retry on the failed message.
3. Observe that Chatwoot changes the message to sent but does not
contact the provider because the previous provider message ID remains
present.

### How to test

1. In an external provider inbox, retry a failed message that has an
existing provider message ID.
2. Confirm the old provider ID is cleared before delivery and the retry
reaches the provider.
3. Confirm the provider assigns a fresh message ID and subsequent status
callbacks update the retried message.
4. Retry a failed API or WebWidget message and confirm its existing
`source_id` is preserved.
5. Trigger concurrent retry requests for the same failed message and
confirm only one delivery job is queued.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-08-13 11:35:52 +05:30
Muhsin Keloth
10e9cb5b60 fix(whatsapp): load health status for regional locales (#15440)
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>
2026-08-13 10:26:06 +05:30
Vishnu Narayanan
3453e2975d chore: land super admin on dashboard after login (#15411)
## Description

After a successful super admin login, redirect to the super admin
dashboard (`super_admin_root_path`) instead of the users list. The
dashboard is a lighter, overview-first landing page, while the users
index does an exact count over the full users table on every load and
can be slow on large instances.

This only changes the post-login landing page. The users list is still
reachable from the navigation.

Fixes https://linear.app/chatwoot/issue/CW-7928

## Type of change

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

## How Has This Been Tested?

Added request specs for `SuperAdmin::Devise::SessionsController#create`:
- successful login redirects to `super_admin_root_path`
- invalid credentials redirect back to the login page

`bundle exec rspec
spec/controllers/super_admin/devise/sessions_controller_spec.rb` -> 3
examples, 0 failures.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [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
2026-08-12 18:35:21 -07:00
Vijay Misal
adef9f2cd6 fix: Alt+E resolve shortcut also opens Chrome's built-in menu on Windows (#15418) 2026-08-12 21:54:34 +05:30
nikita42
42ca0901c2 fix(editor): handle parenthesized ordered list markers (#15362) 2026-08-12 21:49:03 +05:30
Khush Raghav Nanda
958fc37ba1 fix(conversation): refactor layout utilities to logical properties for RTL (#15341) 2026-08-12 21:35:17 +05:30
dependabot[bot]
d310e2c619 chore(deps-dev): bump postcss from 8.4.47 to 8.5.23 (#15380) 2026-08-12 19:09:56 +05:30
Corey
f9cdfe0071 fix: Use dark text on amber solid buttons so the disabled Add Note button stays visible (#15371) 2026-08-12 18:52:46 +05:30
Aakash Bakhle
a47eb375ad feat(captain): add advanced inactivity policy backend (4/5) (#15306)
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>
2026-08-12 18:33:48 +05:30
Aakash Bakhle
121b743f88 fix: scope Captain assistant responses to account (#15386)
## Description

Fixes account scoping for Captain assistant responses.

Create and update accepted an `assistant_id` from the request. The model
then set the response account from that assistant. The controller lookup
read the top level parameter, while the API sends the ID inside
`assistant_response`, and create did not use the lookup result.

The controller now resolves the nested assistant ID through
`Current.account`, removes `assistant_id` before assigning request
fields, and assigns the scoped assistant directly. The model now fills
the account only when it is blank and rejects a response when its
account and assistant do not match.

Linear issue:
[CW-7913](https://linear.app/chatwoot/issue/CW-7913/ghsa-phpm-m2mf-r8r9-captain-assistant-responses-writes-into-another)

## Type of change

- [x] Bug fix

## How has this been tested?

- Ran `bundle exec rspec
spec/enterprise/controllers/api/v1/accounts/captain/assistant_responses_controller_spec.rb
spec/enterprise/models/captain/assistant_response_spec.rb`. All 19
examples passed.
- Ran the two new account isolation examples against the original code.
Both failed and reproduced the create and update issue. Both pass with
this fix.
- Ran RuboCop on the five changed Ruby files. It found no offenses.

## 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 is effective
- [x] New and existing unit tests pass locally with my changes
2026-08-12 17:48:52 +05:30
Tanmay Deep Sharma
1e17cbe0e7 feat(voice): transcribe Twilio call recordings (#15241)
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.
2026-08-12 16:54:49 +05:30
Shivam Mishra
a24f5a3e7a fix: anchor contact phone number validation (#15415)
Contact phone numbers with stray text in front of them, like
`abc+12312312321`, were saving successfully instead of being rejected as
invalid. Agents could end up with unusable numbers on a contact, and the
same values were persisted rather than discarded when captured through
the live chat widget.

## How to reproduce

1. Open a contact and edit its details.
2. Set the phone number to `abc+12312312321` via the API (`PATCH
/api/v1/accounts/:id/contacts/:id`).
3. Before this change the update succeeds. Now it fails validation.

## What changed

The E.164 format check was missing a leading `\A` anchor, so Rails
matched it anywhere in the string and accepted any prefix ahead of a
valid number. Both the validation and the `phone_number_format` fallback
used by `discard_invalid_attrs` are now anchored, so the widget path
discards these values instead of storing them.

Contacts already holding a prefixed number will now fail validation on
their next save. Worth a count on production first:

```sql
SELECT count(*) FROM contacts WHERE phone_number !~ '^\+[1-9][0-9]{1,14}$' AND phone_number <> '';
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-12 15:28:49 +05:30
Muhsin Keloth
79405f76c4 feat(whatsapp): display click-to-chat ad referrals (#15424)
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>
2026-08-12 13:28:33 +05:30
Sivin Varghese
541f91acae fix: allow resizing the reply editor while a Copilot suggestion is active (#15391) 2026-08-12 12:42:31 +05:30
Sivin Varghese
5a02616189 fix: inline editor in the contact sidebar clips longer values (#15403) 2026-08-12 12:32:31 +05:30
Devi R
8397fa7eaf fix: include account_id in FCM push payload (#15260)
## Description

- fcm_push_data didn't include the account id, so mobile clients had no
way to know which account a push notification belonged to. This adds
account_id to the FCM payload (it was already present in push_event_data
for ActionCable, just not FCM).
- Prerequisite for the mobile-side fix for
[chatwoot-mobile-app#1121](https://github.com/chatwoot/chatwoot-mobile-app/pull/1121)
(opening a conversation from a notification for a non-active account).
- Added a spec asserting fcm_push_data includes the account id.

Fixes
[CW-4235](https://linear.app/chatwoot/issue/CW-4235/the-conversation-fails-to-open-when-the-notifications-account-differs)

## Type of change

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

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-08-11 20:10:51 -07:00
Sivin Varghese
7ed7bdcef4 fix: close editor pickers when the composer state is rebuilt (#15396)
# 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
2026-08-11 19:42:23 -07:00
Baptiste Fontaine
07352f5ca0 chore(deps): bump lettersanitizer from 1.0.6 to 1.0.8 (#14959)
# Pull Request Template

## Description

This PR bumps lettersanitizer to 1.0.8, because that release includes a
[patch][1] that fixes an [issue][2] affecting Chatwoot: in some cases,
if you receive a malformed email with deeply-nested tags considered
unvalid by `lettersanitizer`, just selecting the email in your inbox
causes the browser tab or even the whole computer to freeze, due to RAM
exhaustion.

[1]: https://github.com/mat-sz/lettersanitizer/pull/10
[2]:
https://github.com/mat-sz/lettersanitizer/security/advisories/GHSA-hhw2-373g-hx77

Edit: also bump `vue-letter` to 0.2.2 to update its own
`lettersanitizer` dependency
(https://github.com/mat-sz/vue-letter/pull/3), per
https://github.com/chatwoot/chatwoot/pull/14959#discussion_r3546868353.

## Type of change

Please delete options that are not relevant.

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

## How Has This Been Tested?

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


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] ~~I have commented on my code, particularly in hard-to-understand
areas~~
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] 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
- [x] Any dependent changes have been merged and published in downstream
modules
2026-08-11 19:40:42 -07:00
Sony Mathew
8842c71a90 fix: Handle NUL bytes in canned response search (#15397)
Canned response search now removes NUL bytes from user-provided search
text before passing it to PostgreSQL, so malformed input returns normal
search results instead of a database encoding error.

## Closes

-
[CW-7922](https://linear.app/chatwoot/issue/CW-7922/harden-backend-paths-causing-production-sentry-errors)
- [Sentry 7663466064](https://chatwoot-p3.sentry.io/issues/7663466064/)

## How to reproduce

Call the canned responses endpoint with a search parameter containing a
NUL byte. PostgreSQL previously raised `PG::UntranslatableCharacter`
while evaluating the search query.

## What changed

- Strip NUL bytes once at the controller boundary.
- Reuse the sanitized value for matching and result ranking.
- Add request coverage for a search term containing a NUL byte.
2026-08-11 19:14:10 -07:00
dependabot[bot]
10ea370c07 chore(deps): bump dompurify from 3.4.11 to 3.4.13 (#15370)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.11 to
3.4.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.4.13</h2>
<ul>
<li>Fixed an issue with hook removal during <code>IN_PLACE</code>
sanitization, thanks <a
href="https://github.com/koyokr"><code>@​koyokr</code></a></li>
<li>Fixed an issue with hooks potentially bypassing the clone guard,
thanks <a
href="https://github.com/AkshayjainG"><code>@​AkshayjainG</code></a></li>
<li>Fixed an issue with DOM clobbering via <code>ownerDocument</code>
during <code>IN_PLACE</code>, thanks <a
href="https://github.com/AkshayjainG"><code>@​AkshayjainG</code></a></li>
<li>Bumped several dependencies where possible</li>
</ul>
<h2>DOMPurify 3.4.12</h2>
<ul>
<li>Fixed an issue where a hook would not get called for custom
elements, thanks <a
href="https://github.com/Rikuxx0"><code>@​Rikuxx0</code></a></li>
<li>Hardened the handling of hooks removing elements, <a
href="https://github.com/mkrause-bee360"><code>@​mkrause-bee360</code></a></li>
<li>Added support for a few new SVG attributes, thanks <a
href="https://github.com/cbn-falias"><code>@​cbn-falias</code></a> &amp;
<a
href="https://github.com/Develop-KIM"><code>@​Develop-KIM</code></a></li>
<li>Hardened the handling of declarative partial updates</li>
<li>Updated the documentation is several spots, README, wiki, etc.</li>
<li>Bumped several dependencies where possible</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3067f77467"><code>3067f77</code></a>
release: 3.4.13 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1562">#1562</a>)</li>
<li><a
href="a9ca1e5374"><code>a9ca1e5</code></a>
release: 3.4.12 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1537">#1537</a>)</li>
<li>See full diff in <a
href="https://github.com/cure53/DOMPurify/compare/3.4.11...3.4.13">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.4.11&new-version=3.4.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-08-11 18:59:46 -07:00
Marco Cabral
eaecc43ca4 fix: search returning 500 when a conversation has no messages (#15328)
## Description

`SearchService#filter_conversations` matches conversations on the
display id and on the contact name, email, phone number and identifier.
It never looks at message content, so a conversation with no messages is
a valid result whenever its contact matches.

The search views did not account for that. They rendered
`conversation.messages.try(:first)`, which is `nil` for such a
conversation, and `api/v1/models/_message` calls `message.id` on it:

```
ActionView::Template::Error (undefined method 'id' for nil):
    1: json.id message.id
app/views/api/v1/models/_message.json.jbuilder:1
app/views/api/v1/accounts/search/_message.json.jbuilder:1
app/views/api/v1/accounts/search/conversations.json.jbuilder:8
```

A single conversation without messages is enough to turn the whole
search request into a 500 for that query, so the agent loses
conversation search entirely until that conversation gets a message.

Both views that render a conversation search result were affected, so
this applies to `GET /search/conversations` and to the combined `GET
/search`.

The fix guards the message partial the same way the neighbouring
`contact`, `inbox` and `agent` partials in those same views are already
guarded. When there is no message the key is rendered as an empty
object, which is what already happens for a missing contact, inbox or
assignee.

**How to reproduce**

1. Create a conversation without any message (for example via `POST
/api/v1/accounts/{id}/conversations` without a `message`).
2. Search for the contact's name or phone number: `GET
/api/v1/accounts/{id}/search/conversations?q=<phone>`.
3. The request returns 500.

## Type of change

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

## How Has This Been Tested?

Added one spec per affected endpoint in
`spec/controllers/api/v1/accounts/search_controller_spec.rb`, each
creating a conversation with no messages whose contact matches the query
and asserting that it is returned. Both fail with a 500 before the
change.

Also reproduced manually on a running instance: searching a contact that
had a conversation with no messages returned 500, and returns 200 with
`"message": {}` after the change.

## Checklist:

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

---

Related: #15289 documents the atomic `POST /conversations` with an
inline `message`, which avoids creating conversations without messages
in the first place. This fix is independent of it — it protects the
search regardless of how the conversation ended up without messages
(created by an agent before replying, by an API integration, by campaign
tooling, or by a flow that did not complete).

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-08-11 18:34:21 -07:00
Muhsin Keloth
a4ce4b1dc3 fix(contacts): enqueue avatar sync after commit (#15402)
Contact avatars could be skipped when an avatar job was enqueued while
the contact record was still inside an open database transaction. A fast
worker could pick up the job before the record became visible and
discard it after deserialization failed.

This change makes avatar URL jobs wait for the surrounding transaction
to commit before they enter the queue. Avatar jobs scheduled outside a
transaction continue to enqueue immediately, and the existing download,
validation, and rate-limiting behavior remains unchanged.

Fixes
https://linear.app/chatwoot/issue/CW-7917/facebook-contact-avatars-never-sync-avatarfromurljob-is-enqueued

### How to reproduce

1. Receive a Facebook message from a sender who does not yet exist as a
contact.
2. Let the message flow create the contact and schedule its avatar job
within the same transaction.
3. Observe that a worker can attempt to deserialize the contact before
the transaction commits, causing the avatar job to be discarded.

### How to test

1. Receive a Facebook message from a new sender with a profile picture.
2. Confirm the contact and conversation are created successfully.
3. Confirm the avatar job is enqueued after the transaction commits and
the profile picture is attached.
4. Create a contact with an avatar through the Contacts API and confirm
the same behavior.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-08-11 17:13:11 -07:00
Sivin Varghese
3f4d28f77b feat: add search and preview to the mention, variable and emoji pickers (#15375)
# 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
2026-08-11 18:56:52 +05:30
Sivin Varghese
7cba8a01bd feat: add search to filter dropdowns and group attributes (#15348) 2026-08-11 18:48:55 +05:30
Vishnu Narayanan
e6f21f7c99 perf: scale sidekiq worker MemoryMax with host memory (#15412)
## Description

This systemd unit is installed on every self-hosted Linux install
(copied by `deployment/setup_18.04.sh` / `setup_20.04.sh`) as well as
our own worker hosts, so the memory cap has to be correct across a wide
range of box sizes.

It is currently a fixed `MemoryMax=1.2G`, chosen when workers ran on 2GB
hosts (#12915). On larger hosts that fixed cap sits at a high fraction
of the limit under normal load, so transient spikes hit the hard limit
and the worker is OOM-restarted more often than necessary (each restart
drops the jobs in-flight at that instant).

Switching to `MemoryMax=60%` makes the cap scale with the host's
physical memory instead of being fixed:

- 2GB host: 60% = 1.2G, identical to today (no regression for small
installs)
- 4GB host: 60% = ~2.4G, room to absorb spikes without a premature
restart
- larger hosts scale proportionally

`MemoryHigh=infinity` is unchanged, so the worker is still never
throttled by cgroup reclaim (kept intentionally since #12871 to avoid
reclaim-induced worker stalls). Only the hard cap changes, and only its
scaling.

Fixes https://linear.app/chatwoot/issue/INF-100

## Type of change

- [x] Performance (non-breaking change which improves performance)
2026-08-11 15:46:05 +05:30
Shivam Mishra
875ea6e88f fix(auth): persist MFA sessions across browser restarts (#15379)
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`.
2026-08-11 15:03:49 +05:30
Shivam Mishra
0a2293f921 feat: record Captain conversation outcome episodes from lifecycle events [CW-7792] (#15316)
Records Captain conversation outcomes at episode grain so reporting can
distinguish initial demand from reopened conversations and measure
replies, handoffs, resolutions, human follow-up, and CSAT.

Eligibility creates the episode at demand time. Message-derived fields
are snapshotted from persisted messages at handoff or resolution,
keeping terminal analytics accurate without writing outcomes for every
message. Outcome tracking remains reporting-only and fail-open.

Builds on the episode-grain schema from #15315.

## Closes

- https://linear.app/chatwoot/issue/CW-7792

## How to test

1. Enable `captain_integration_v2` and connect a Captain assistant to an
inbox.
2. Send an inbound customer message and confirm an initial outcome
episode is created at the message timestamp.
3. Let Captain reply and then resolve or hand off the conversation.
Confirm the episode records Captain reply counts and timestamps, the
outcome timestamp, and the handoff category where applicable.
4. Reply after resolution and confirm a `reopen` episode is created
while preserving the previous episode.
5. Resolve the reopened conversation and submit CSAT. Confirm the
response is attributed to the episode that issued the survey.

## What changed

- Creates the initial episode from demand-level eligibility and appends
a new episode when a resolved conversation reopens.
- Snapshots Captain replies and the first qualifying human reply from
persisted messages at handoff and resolution.
- Attributes asynchronous resolution events using the episode active at
the event timestamp.
- Records later CSAT responses using the survey message timestamp.
- Keeps boundary writes transactional and fail-open without retries,
advisory locks, late-boundary repair, or handoff self-healing.
- Adds schema-constrained handoff reason categories, including lifecycle
coverage for incomplete V2 tool fallback handoffs.

Open, non-terminal episodes may retain empty or stale message-derived
fields until handoff or resolution.
2026-08-11 14:57:23 +05:30
Aakash Bakhle
1693525124 fix(captain): scope copilot conversation access (#15249)
## Description

Captain Copilot's `get_conversation` tool returned any conversation in
the account, without checking whether the agent asking for it could open
that conversation from the inbox view. An agent who belongs to a single
inbox, or who holds a narrow custom role, could therefore read the
message history of conversations outside their access, including the
private notes on them.

The tool now runs the same permission filter that the conversation list
endpoint and Copilot's own `search_conversation` tool already use, so it
returns only the conversations the agent can already open.
Administrators see no change, because the filter returns the whole
account scope for them.

The Copilot chat service had the same gap. When an agent opened Copilot
while viewing a conversation, the service looked that conversation up by
account alone and wrote its ID and contact ID into the system prompt. It
now resolves the conversation through the same filter, and leaves the
context out when the agent cannot access it.

## Closes

https://linear.app/chatwoot/issue/CW-7768

## Type of change

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

## How to reproduce

1. Create two inboxes in one account, for example Inbox A and Inbox B.
2. Add an agent to Inbox A only, then start a conversation in Inbox B
and leave a private note on it.
3. Sign in as that agent, open Copilot, and ask it for the Inbox B
conversation by its ID.
4. Before the change Copilot returns the full message history including
the private note. After the change it reports that the conversation was
not found.

Opening the same conversation from the inbox view as that agent is
rejected both before and after the change, so the inbox view and Copilot
now agree on what the agent can read.

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-08-11 14:49:59 +05:30
Tanmay Deep Sharma
406eb8f650 feat(automation): allow send email transcript action to target the contact's email (#15373)
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>
2026-08-11 10:44:27 +05:30