Commit Graph
149 Commits
Author SHA1 Message Date
Tanmay Deep SharmaandGitHub f6c18f5225 feat: account calls dashboard index endpoint (#14780)
## Description

Adds a backend endpoint that powers an account-wide calls dashboard,
letting users list and filter all calls in the account.

## Linear Ticket
- https://linear.app/chatwoot/issue/UPM-28/voice-call-dashboard-view

## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## 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
2026-07-08 15:31:03 +05:30
a5fcecb3f6 feat: assistant overview page [CW-7408] (#14889)
This PR adds a Captain Assistant **Overview** page to show some KPI
metrics (conversations handled, auto-resolution, handoff, hours saved,
reopen-after-resolve, conversation depth) with trend deltas vs the
previous window, a real knowledge card, and a lazily-loaded, cached LLM
welcome summary.

### Highlights

- **Two contextual banners** on the overview:
- **Inbox banner** — prompts the user to connect an inbox when the
assistant has none, so it can actually do work.
- **Coverage banner** — warns when FAQ coverage is below 85% with more
than 100 responses pending review, linking straight to the pending
queue. Dismissal persists per-assistant for 24h via localStorage.
- **Batched stats builder** (`Captain::AssistantStatsBuilder`) computes
both windows in single FILTER-aggregated scans to cut round trips,
behind new `stats`/`summary` endpoints.
- **Cards included but intentionally left dummy / not rendered yet:**
`ResponseQualityCard` (flagged responses) and `CreditUsageCard` (credit
usage + daily chart). Credits are an account-wide counter with no
per-assistant or daily history, so there is no real data to back them
yet; they ship in the codebase but are not wired into the page.

### Index migration

- Replaces `index_messages_on_sender_type_and_sender_id` with
`index_messages_on_sender_and_created` `(sender_type, sender_id,
created_at)`.
- **Why it helps:** the per-assistant windowed lookups filter `sender_*`
*and* a `created_at` range. The old 2-column index matched every
lifetime row for the assistant and filtered the time slice at the heap
(~89% of rows discarded); adding `created_at` as a range column lets
Postgres scan only the window, and fixes the row-count estimate so the
planner picks a hash join over a nested loop on `reporting_events`.
- **Why dropping the old index is safe:** the new index is a left-prefix
superset `(sender_type, sender_id, ...)`, so every query the old one
served is still served. No code references it by name, and dropping it
keeps write amplification on `messages` neutral. Built/dropped with
`CONCURRENTLY` and `if_not_exists`/`if_exists` guards.


## Preview

<img width="2572" height="1754" alt="CleanShot 2026-06-29 at 22 38
51@2x"
src="https://github.com/user-attachments/assets/3798d09e-7850-48e4-b2cd-508533f15cea"
/>

## Banners

#### Inbox connect alert

<img width="2178" height="612" alt="CleanShot 2026-06-30 at 14 26 55@2x"
src="https://github.com/user-attachments/assets/373c371c-bb7d-4291-a0f9-620673078302"
/>

#### Coverage alert
<img width="2178" height="612" alt="CleanShot 2026-06-30 at 14 25 41@2x"
src="https://github.com/user-attachments/assets/e12d6308-11b6-4ba2-88a2-8a3077dd3e8f"
/>

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-07-07 17:54:13 +05:30
Tanmay Deep SharmaandGitHub 11deffdd5d feat: billing brl pix new users (#14617)
## Linear ticket
- https://linear.app/chatwoot/issue/CW-7253/billing-brl-pix-new-users

## Description

New accounts that sign up in Brazilian Portuguese are now billed in BRL
instead of USD. Their Stripe customer is created with a Brazil address
and Portuguese locale (so the Stripe portal offers Real prices and PIX),
and the AI credit top-up flow shows packages priced in the account's
billing currency. Currency support is config-driven, so adding another
currency later is a configuration change rather than a code change.


## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- https://www.loom.com/share/c8d3d08c1b844ed6b820438d4209491a

## Screenshot
<img width="904" height="440" alt="image"
src="https://github.com/user-attachments/assets/6f19fad8-e6af-46ea-b99f-b0265bb9eeec"
/>


## 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
2026-07-06 16:35:38 +05:30
6c9efc4e92 fix: assign outbound voice call conversation to the calling agent (#14906)
Outbound voice calls were being auto-assigned to the wrong agent. When
an agent placed an outbound call, the conversation was created without
an assignee, so inboxes with auto-assignment enabled would round-robin
it to a different agent instead of keeping it with the person who
actually made the call. This made it hard to tell which agent was on an
active call.

## What changed

- Set the calling agent as the conversation's assignee when creating an
outbound voice call conversation.
- This prevents the generic auto-assignment handler from treating the
conversation as unassigned and reassigning it.
- Added specs covering the assignment, including a regression case with
inbox auto-assignment enabled.

**Note:** this applies to newly placed calls; it does not retroactively
fix conversations that were already mis-assigned.

---------

Co-authored-by: Tanmay Deep Sharma <tanmaydeepsharma21@gmail.com>
Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
2026-07-02 16:06:11 +05:30
7bf76057c2 fix: SLA handling for blocked contacts (#14861)
# Pull Request Template

## Description

Blocked contacts are now excluded from SLA assignment, processing,
reports, and conversation SLA UI while they remain blocked. Existing SLA
records are preserved, and SLA behavior resumes if the contact is
unblocked.

Fixes
https://linear.app/chatwoot/issue/CW-7435/sla-should-not-trigger-for-blocked-contacts

## Type of change

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

## How Has This Been Tested?

- `bundle exec rspec spec/enterprise/models/conversation_spec.rb
spec/enterprise/models/applied_sla_spec.rb
spec/enterprise/services/enterprise/action_service_spec.rb
spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb
spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb
spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/presenters/conversations/event_data_presenter_spec.rb` —
78 examples, 0 failures
- `bundle exec rubocop
enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb
enterprise/app/jobs/sla/process_account_applied_slas_job.rb
enterprise/app/models/applied_sla.rb
enterprise/app/models/enterprise/concerns/conversation.rb
enterprise/app/presenters/enterprise/conversations/event_data_presenter.rb
enterprise/app/services/enterprise/action_service.rb
enterprise/app/services/sla/evaluate_applied_sla_service.rb
lib/tasks/apply_sla.rake
spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb
spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb
spec/enterprise/models/applied_sla_spec.rb
spec/enterprise/models/conversation_spec.rb
spec/enterprise/presenters/conversations/event_data_presenter_spec.rb
spec/enterprise/services/enterprise/action_service_spec.rb
spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb` — no
offenses
- `pnpm exec vitest --no-watch --no-cache --no-coverage
app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js`
— 2 tests passed
- `pnpm exec eslint
app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue
app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCardExpanded.vue
app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue
app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js`
— passed with existing raw-text warnings in `ConversationHeader.vue`
- `git diff --cached --check` — clean

## 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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-07-02 14:16:29 +05:30
Tanmay Deep SharmaandGitHub a3c7f3b204 fix: finalize WhatsApp calls when terminate webhook overtakes connect (#14836)
## Description

Some inbound WhatsApp calls stayed stuck in "ringing" forever. When a
caller hung up within ~1s of dialing, Meta delivered the terminate
webhook before the connect webhook. The terminate arrived with no call
record yet and was dropped, then connect created the call in ringing
with nothing left to close it. These calls now correctly land as missed
(no_answer), and an agent who taps Accept on a call that already ended
gets a clean "call ended" instead of a generic error.

## Type of change

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

## How Has This Been Tested?

- local UI testing

## 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
2026-07-02 13:58:36 +05:30
62cbeae95f feat: onboarding inboxes UI (#14565)
After entering their account details, new admins land on an **Inbox
setup** screen that shows what we've already set up for them and lets
them connect their conversation channels without leaving onboarding. It
surfaces the auto-created live chat widget (and Help Center on
Enterprise), highlights channels detected from their website, and offers
a **View all** dialog to connect any supported channel inline.

### Channel status

| Channel | How it connects | Status | PR |
|---|---|---|---|
| Live chat (Website) | Auto-created during setup |  Done |
https://github.com/chatwoot/chatwoot/pull/14314 |
| WhatsApp | Meta embedded signup |  Done |
https://github.com/chatwoot/chatwoot/pull/14619 |
| Facebook | Login + page picker |  Done |
https://github.com/chatwoot/chatwoot/pull/14619 |
| Instagram | OAuth redirect |  Done |
https://github.com/chatwoot/chatwoot/pull/14568 |
| TikTok | OAuth redirect |  Done |
https://github.com/chatwoot/chatwoot/pull/14569 |
| LINE | Inline credential form |  Done | — |
| Telegram | Inline credential form |  Done | — |
| Gmail / Outlook | OAuth (email) | ⚠️ Disabled — coming in a follow-up
| https://github.com/chatwoot/chatwoot/pull/14567 |
| SMS / API / Voice / Other email | — |  Unavailable | — |

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-25 14:54:07 +05:30
Sojan JoseandGitHub cd1a214d98 chore: Add marketing conversion tracking foundation (#14851)
## Summary

Adds the minimal foundation for Cloud marketing conversion tracking:

- locked internal installation config for conversion tracking
credentials and event mappings
- generic background job and service for uploading conversion events
from stored attribution
- focused coverage for Cloud gating, click-id selection, payload shape,
and optional conversion value

This PR intentionally does not wire signup or plan activation yet. The
next step is to validate the service from Rails console against existing
attributed accounts, then add the event hooks in a follow-up PR.

## Notes

The config remains locked and is surfaced under the Internal settings
group next to the existing Cloud plan configuration. The service assumes
the locked config is present and valid; config mistakes should surface
instead of being silently ignored.
2026-06-24 18:27:02 -07:00
647cfc2d83 feat: distinguish agent-declined calls with a rejected status (#14783)
## Linear ticket
-
https://linear.app/chatwoot/issue/CW-7374/agent-declined-voice-calls-miscounted-as-failed

## Description

Agent rejections were stored with status: failed, so call reports lumped
deliberate declines together with real technical failure.

Fix: give declines their own terminal rejected status (Twilio + WhatsApp
paths), keeping end_reason: agent_rejected. Genuine provider/network
failures stay failed. Frontend renders declines exactly as before;
existing rows backfilled via migration.

## Type of change

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

## How Has This Been Tested?

- Tested on UI

## Checklist:

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

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-23 12:22:29 +05:30
Sojan JoseandGitHub e86222034e fix(auth): record attribution for oauth signups (#14796)
Google OAuth signups now persist the same first-party attribution
cookies as email signups on Chatwoot Cloud.

This keeps attribution capture owned by the website and reuses the
existing Enterprise-only account attribution service. The OAuth callback
only records the already-shaped first-touch and last-touch cookie
payload after a new account is created.

## What changed

- Added Enterprise-only attribution persistence to the Google OAuth
signup account creation path.
- Reuses `Internal::Accounts::MarketingAttributionService`.
- Keeps attribution best-effort so failures do not interrupt OAuth
signup.
- Leaves existing email signup and SAML behavior unchanged.

## How to test

- Start from a Chatwoot Cloud-like setup with attribution cookies
present.
- Sign up using the Google OAuth button.
- Confirm the created account has
`internal_attributes['marketing_attribution']` with `first_touch` and
`last_touch`.
- Confirm existing Google OAuth login still redirects normally.

Validation run locally:

- `bundle exec rspec
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
- `bundle exec rspec
spec/controllers/devise/omniauth_callbacks_controller_spec.rb
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
- `bundle exec rubocop
enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
2026-06-22 06:51:09 -07:00
2b37fef0e0 fix(companies): sync contact company names (#14759)
Fixes company-contact name drift when a company is renamed or deleted.

Closes: N/A

## Why
Contacts keep a denormalized `additional_attributes.company_name` for
display and filtering. Company rename/delete flows could leave that
copied value stale even though the actual `company_id` relationship
changed.

## What changed
- Enqueues an async company contact-name sync job when a company name
changes.
- Moves company deletion into `Companies::DeleteJob`.
- The delete job unlinks linked contacts, clears only the copied
`company_name`, and then deletes the company.
- Uses bulk JSON updates for the cleanup path so contact records are not
saved, which avoids contact update callbacks, webhook dispatch, and
automation side effects.

## How to test
- Link a contact to a company, rename the company, and confirm the
contact company name updates after the job runs.
- Delete a company with linked contacts and confirm the delete job
removes the company, unassigns linked contacts, and preserves other
contact additional attributes.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-22 06:49:52 -07:00
6a262287d2 feat(captain): allow agents to report Captain messages (#14799)
Adds a cloud-only flow for agents to flag incorrect or problematic
Captain (AI) responses. Right-clicking a Captain message surfaces a
"Report message" option that opens a dialog to pick a problem type and
add a description, persisted to a new captain_message_reports table for
the team to review.


<img width="636" height="542" alt="Screenshot 2026-06-20 at 9 15 56 AM"
src="https://github.com/user-attachments/assets/afaa233d-6bd6-455a-8a33-a3796a3e3ef6"
/>
<img width="580" height="502" alt="Screenshot 2026-06-20 at 9 16 03 AM"
src="https://github.com/user-attachments/assets/2d220d99-98cc-4c5e-a325-778ceb4f7bc9"
/>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:47:08 -07:00
Sojan JoseandGitHub dfd656a7d9 feat(account): persist signup attribution (#14761)
This keeps Chatwoot-side attribution persistence intentionally small and
Enterprise-only.

The website owns attribution capture, normalization, and source
classification. Chatwoot Cloud only reads the already-shaped first-party
attribution cookies during the current web signup account creation path
and stores the decoded payload in internal account metadata. Self-hosted
installs remain unchanged in this repo.

## What changed

- Added Enterprise-only attribution persistence to the current Cloud web
signup account creation path.
- Stores attribution only when `ChatwootApp.chatwoot_cloud?` is true.
- Reads the existing first-touch and last-touch attribution cookies.
- Saves only the documented scalar attribution fields under account
internal metadata.
- Preserves raw attribution values and leaves escaping to display
boundaries.
- Bounds stored attribution values to the website field-size limit.
- Keeps OSS controller code unchanged.
- Keeps signup attribution request coverage in Enterprise specs.
- Avoids backend attribution derivation, referrer parsing, or fallback
classification.
- Skips authenticated add-workspace flows so additional workspaces are
not counted as signup attribution.
- Does not hook unused account creation paths or OmniAuth account
creation.

## How to test

- `bundle exec rubocop
spec/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts_controller_spec.rb
enterprise/app/controllers/enterprise/api/v1/accounts_settings.rb
enterprise/app/services/internal/accounts/marketing_attribution_service.rb
spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb`
- `bundle exec rspec spec/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb`
- On a cloud-like setup, create an account through the current web
signup path with attribution cookies and confirm account internal
metadata is populated.
- On a non-cloud setup or authenticated add-workspace flow, confirm
account-create behavior is unchanged and no attribution is stored.
2026-06-19 11:01:12 -07:00
41a3ab6dfa feat(companies): add contact company selector (#14496)
Adds a company selector to the contact details form so agents can
associate a contact with an existing company directly from the contact
page.

Closes

- None

Why

Contacts already expose company information through the CRM fields, but
the form only accepted free-text company names. As we split company CRM
work into smaller PRs, this keeps the contact page aligned with the
structured company model while preserving the existing company-name
behavior used by automations.

What changed

- Shows a company dropdown in the contact details form when the
Companies feature is enabled.
- Keeps legacy free-text company names editable when a contact has no
structured `company_id`.
- Allows Enterprise contact create/update APIs to accept account-scoped
`company_id`.
- Syncs `additional_attributes.company_name` when a contact is
associated with a company, including the existing email-domain
auto-association path.
- Serializes `company_id` in the contact model payload so the form can
show the current association.

How to test

1. Enable Companies for an account and open a contact details page.
2. In Edit contact details, use the Company field to select an existing
company.
3. Save the contact and refresh the page.
4. Confirm the selected company remains visible and the contact is
associated with that company.
5. Confirm contacts with only a legacy free-text company name still show
the text input instead of an empty selector.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-16 15:29:27 +05:30
Tanmay Deep SharmaandGitHub 8d5d02ea97 feat: add per-inbox toggle to disable incoming calls (#14645)
## Description

Adds a per-inbox "Allow incoming calls" toggle for voice-enabled
WhatsApp and Twilio inboxes. When turned off, the setting is persisted
on the channel; actually rejecting inbound calls is handled in a
follow-up PR.

## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## Screenshot
<img width="804" height="384" alt="Screenshot 2026-06-04 at 11 44 07 AM"
src="https://github.com/user-attachments/assets/df8bb026-0387-4031-bcba-6d9a56872eb7"
/>


## 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
2026-06-11 14:22:44 +05:30
Shivam MishraandGitHub d9c07fe2e9 feat: expose onboarding help center generation status (#14671)
This PR adds a reload-safe onboarding Help Center generation status
path. Previously, generation progress was pushed through ActionCable,
which meant the onboarding UI could lose context after a page reload or
missed websocket event. The new endpoint exposes the current generation
id, raw Redis generation state, and Help Center article/category counts
so clients can recover state by fetching from the backend.

**What changed**

- Added an onboarding Help Center generation status endpoint.
- Persisted `help_center_generation_id` when generation is enqueued.
- Removed Help Center generation ActionCable broadcasts completely.
- Kept generation progress in Redis as the backend source of truth.
- Kept Help Center generation out of the onboarding hot path; the
existing onboarding controller does not start generation yet.

**How to test**

1. Start Help Center generation for an account.
2. Fetch the onboarding generation status endpoint and verify it returns
the generation id, Redis state, and article/category counts.
3. Reload the onboarding UI or client state and fetch again to confirm
progress can be recovered without relying on websocket events.
4. Verify skipped/completed generation states are reflected from Redis.

## Related PRs

- https://github.com/chatwoot/chatwoot/pull/14569
- https://github.com/chatwoot/chatwoot/pull/14568
- https://github.com/chatwoot/chatwoot/pull/14567
- https://github.com/chatwoot/chatwoot/pull/14619
- https://github.com/chatwoot/chatwoot/pull/14565 (Primary onboarding
PR)
2026-06-10 16:24:37 +05:30
Shivam MishraandGitHub 3dfb5061e1 refactor: route captain custom tool requests through SafeFetch (#14620)
Captain custom tools previously used their own hand-rolled `Net::HTTP`
request code. This moves them onto `SafeFetch`, the same shared
HTTP-fetching helper already used by webhooks, uploads, avatar imports,
and the Captain page crawler. Behavior for normal tools is unchanged —
they just now share one consistent path for host resolution, timeouts,
response size limits, and redirect handling.

**What changed**
- `HttpTool#execute_http_request` now delegates to `SafeFetch.fetch`
instead of building `Net::HTTP` requests by hand. Auth headers, basic
auth, metadata headers, JSON content-type, and the 1 MB response cap all
map onto `SafeFetch` options.
- Removed ~80 lines of bespoke request/validation plumbing from
`HttpTool`.
- The custom tools `test` endpoint now reads the response body string
directly (the executor returns the body rather than a response object).

**How to test**
1. Enable `custom_tools` (or `captain_integration_v2`) for an account.
2. Create a Captain custom tool pointing at a public HTTPS endpoint
(e.g. a test API).
3. Use the **Test** button in the tool form — you should get a success
result.
4. Run the tool from a Captain conversation and confirm the response is
returned/templated as before.

**Gotchas**
- **Local dev:** `SafeFetch` blocks requests to private/loopback
addresses by default. If you're testing a custom tool against a service
on `localhost` or a private IP during development, set
`SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true` or the request will be rejected.
(This matches how the rest of `SafeFetch` already behaves locally.)
- **Test endpoint status field:** on success the `test` response now
reports `status: 200` rather than the exact 2xx code (201/204/etc.),
because `SafeFetch` signals success-vs-failure rather than exposing the
raw response. The UI only checks the 2xx range, so this is invisible
there — but worth knowing if anything consumes the API directly.
- **Non-2xx responses** still surface as an error (same as before), now
via `SafeFetch::HttpError`.
2026-06-10 14:01:30 +05:30
ecd9c26c8c feat: Implemented search results page functionality (#11086)
# Pull Request Template

## Description
Implemented search results page functionality. Now you can press "Enter"
to search by term and display results in a results page. Also now you
can link to /hc/{account}/en/search?query=XXXXXX to view search results
for XXXXXX query.

fixes: https://github.com/chatwoot/chatwoot/issues/10945

## Screenshots

Classic layout search results:
<img width="3840" height="2160" alt="classic-results"
src="https://github.com/user-attachments/assets/3bbb3272-33ca-4eb4-b80a-76ed77442088"
/>

Classic layout pagination:
<img width="3840" height="2160" alt="classic-page-two"
src="https://github.com/user-attachments/assets/062b09d3-7c58-4d3b-8611-b94375e7db51"
/>

Classic layout empty search:
<img width="3840" height="2160" alt="no-results"
src="https://github.com/user-attachments/assets/c5e3f47a-cd9a-4e14-ae92-ccba00c89e98"
/>

Documentation layout search results:
<img width="3840" height="2160" alt="documentation-results"
src="https://github.com/user-attachments/assets/9e45d8d9-c975-4589-b6c6-3bc7bb3c588e"
/>

Documentation layout dark theme:
<img width="3840" height="2160" alt="documentation-dark"
src="https://github.com/user-attachments/assets/cdb6ed63-4241-4b32-9f79-7d92ed479fc8"
/>

Plain embedded dark layout:
<img width="3840" height="2160" alt="plain-embedded-dark"
src="https://github.com/user-attachments/assets/7deb02b9-9f24-48fb-8979-a2ecd7002c05"
/>

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
2026-06-02 15:19:23 +05:30
Vishnu NarayananandGitHub 6c8741b314 fix: increase audit log page size (#14582)
Audit logs now return up to 25 records per page instead of 15. This
reduces page turns for admins and API consumers while keeping the page
size server-defined.

Fixes
https://linear.app/chatwoot/issue/CW-7172/allow-an-option-to-fetch-more-audit-logs
2026-05-28 12:35:44 +05:30
Tanmay Deep SharmaandGitHub 68e358d732 feat: voice-call UX fixes (#14579)
## Linear ticket
https://linear.app/chatwoot/issue/CW-7187/voice-calls-followup-tasks

## Description

Improvements to the WhatsApp voice-calling experience plus a cheaper,
more accurate audio-transcription model.
- First-time callers now get a real name. An inbound WhatsApp call
creates the contact from the caller's WhatsApp profile name instead of
the bare phone number.
- Clear, consistent call attribution. Call bubbles show a unified
"Handled by {agent}"
- Cleaner call widget. The dismiss (✕) button is shown only for incoming
calls
- WhatsApp calling for manual inboxes. voice_calling_supported? now
covers any whatsapp_cloud inbox
- Transcription: whisper-1 → gpt-4o-mini-transcribe.

## Type of change

- [ ] New feature (non-breaking change which adds functionality)


## 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
2026-05-27 16:16:16 +05:30
d20950c5b4 feat: scheduler fairness [AI-159] (#14425)
# Pull Request Template

## Description
Better scheduling and queueing mechanics for document auto-sync
- add jitter plan wise for document sync
- move auto-sync documents to purgeable queue

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

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

## Checklist:

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-27 16:01:51 +05:30
c4a6a19e9b feat(voice): WhatsApp Cloud Calling — UI [6] (#14346)
## Summary

Frontend for WhatsApp Cloud Calling: header / contact-panel call
buttons, ringing widget, accept/reject/hangup, mute, in-bubble audio
player + transcript, recording-on-hangup upload, mid-call reload
warning. WebRTC is browser-direct to Meta — no media server bridge.

## Closes
- https://linear.app/chatwoot/issue/PLA-150

## How to test

Requires backend support — the controller, services, model changes, and
routes ship in **#14334** (`feature/pla-150`). Merge / deploy that first
(or simultaneously); the FE alone won't function without those
endpoints.

Then on staging, for a WhatsApp Cloud + embedded-signup inbox with the
new \`Configuration → Enable voice calling\` toggle ON and webhook
registered:

1. **Outbound** — open a conversation, click the phone icon in the
conversation header (or contact panel), grant mic, your phone rings,
answer, audio both ways, hang up. Recording + transcript land in the
bubble within ~10s.
2. **Inbound** — call the business number from your phone. The
FloatingCallWidget appears bottom-right with caller name. Click accept,
audio both ways, hang up. Recording + transcript appear.
3. **Mute** — during an active WhatsApp call, click the mic icon next to
hangup. Speech stops reaching Meta until you click again.
4. **Mid-call reload guard** — try `Cmd-R` during an active call;
browser shows a confirm prompt.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-05-22 18:42:39 +05:30
b8deb89613 fix: make SAML callback session independent (#14467)
This PR makes SAML login independent of Rails session cookies

## Problem

The normal SAML login flow should be straightforward:

- User opens Chatwoot.
- Chatwoot creates `_chatwoot_session`.
- User starts SSO.
- Chatwoot redirects the browser to the SAML provider.
- The provider authenticates the user.
- The provider sends the browser back to Chatwoot's ACS URL.
- Chatwoot reads the SAML response, finds or creates the user, and logs
them in.

The fragile step is the ACS callback. Most SSO flows return to the app
through browser redirects where cookies usually pass through as
expected. **ADFS commonly returns the SAML response with a cross-site
POST**. With Chatwoot's session cookie using `SameSite=Lax`, browsers
may not send `_chatwoot_session` on that POST.

SAML validation itself does not need the old Rails session cookie. The
problem was our callback handoff after validation. DeviseTokenAuth
stores the verified OmniAuth payload in Rails session, then redirects to
a second callback route. If the browser does not preserve that session,
Chatwoot has already received a valid SAML response but can no longer
finish login.

## Solution

This PR removes the session-backed handoff for SAML only:

- The SAML callback completes login in the same request where OmniAuth
validates the SAML response.
- Chatwoot reads the verified auth payload directly from
`request.env['omniauth.auth']`.
- Account context and RelayState come from callback params or OmniAuth
env data, not Rails session.
- Other OmniAuth providers continue using the existing DeviseTokenAuth
flow.
- Mobile SAML still works when the IdP returns `RelayState=mobile`; the
callback redirects to the mobile deep link with the generated SSO token.

The previous SAML override used `303 See Other` to avoid replaying the
SAML POST into the second callback route. This change keeps that intent,
but removes the second callback route for SAML entirely.

## Screen recording

### SP Initiated

https://github.com/user-attachments/assets/b0735e93-3864-4cc3-b6fc-419fff4b549e

### IDP Initiated

https://github.com/user-attachments/assets/3ded0246-933c-4c85-9b7c-fa15fdc34883

## Testing

Manual validation:

- Complete a SAML login.
- In the browser network trace, find the IdP POST to
`/omniauth/saml/callback?account_id=<account-id>`.
- Confirm it redirects directly to `/app/login?...sso_auth_token=...`
for web login.
- For mobile, confirm `RelayState=mobile` redirects to the configured
mobile deep link.
- Confirm there is no intermediate `/auth/saml/callback` request.

Testing with mocksaml.com:

- Configure Chatwoot with a public `FRONTEND_URL`.
- Set the mocksaml ACS URL to:

```text
https://<chatwoot-host>/omniauth/saml/callback?account_id=<account-id>
```

- Set the mocksaml audience/SP entity ID to the value shown in Chatwoot
SAML settings, usually:

```text
https://<chatwoot-host>/saml/sp/<account-id>
```

- Use an email returned by mocksaml that exists in the SAML-enabled
account.
- Start login from Chatwoot's SSO login page.
- Confirm the callback redirects directly to the app login URL with an
SSO token.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-18 12:52:45 +05:30
de696a55cb feat(voice): add WhatsApp inbound call webhook pipeline [3] (#14315)
Adds the server-side flow that turns Meta WhatsApp Cloud Calling
webhooks into Chatwoot Calls, conversations, voice_call message bubbles,
and ActionCable broadcasts. Stacked on top of #14312 (PR-2 — provider
methods); intentionally does not include the HTTP controller, routes, or
frontend (those land in PR-4 and PR-9).

## Closes
- Part of the WhatsApp Cloud Calling rollout. Linear: TBD

## What changed

**Webhook routing**
- `app/jobs/webhooks/whatsapp_events_job.rb` — append
`prepend_mod_with('Webhooks::WhatsappEventsJob')` so EE can extend it
without forking.
- `enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb` (new)
— overlay that prepends `handle_message_events` to intercept `field:
'calls'` payloads (route to `Whatsapp::IncomingCallService`) and
`interactive.call_permission_reply` messages (route to
`Whatsapp::CallPermissionReplyService`); falls through with `super` for
regular messages.

**Services**
- `enterprise/app/services/whatsapp/incoming_call_service.rb` (new) —
gated on `provider_config['calling_enabled']`; processes `connect`
(creates inbound call via `Voice::InboundCallBuilder` or transitions an
existing outbound call to `in_progress`) and `terminate` events; updates
conversation `additional_attributes` and broadcasts
`voice_call.incoming`/`voice_call.outbound_connected`/`voice_call.ended`.
- `enterprise/app/services/whatsapp/call_permission_reply_service.rb`
(new) — handles WhatsApp interactive `call_permission_reply` replies;
clears the conversation's `call_permission_requested_at` flag and
broadcasts `voice_call.permission_granted` so the agent UI can re-enable
the call button.

**Builder/model adjustments**
- `enterprise/app/services/voice/inbound_call_builder.rb` —
provider-agnostic; accepts `provider:` and `extra_meta:` kwargs, drops
`account:` (now derived from `inbox.account` to keep the param count
under rubocop's ceiling without disabling cops), uses digits-only
`source_id` for WhatsApp ContactInbox (validation requires
`^\d{1,15}\z`), skips Twilio-only `conference_sid` for non-Twilio
providers.
- `enterprise/app/services/voice/call_message_builder.rb` — adds
`create!`/`update_status!` API and `CALL_TO_VOICE_STATUS` map; uses
direct `Message.create!` (bypasses `Messages::MessageBuilder`'s
incoming-on-non-Api-inbox guard, which would otherwise reject the system
bubble); content is `'WhatsApp Call'` for WhatsApp and `'Voice Call'`
for Twilio. Backwards-compatible `perform!` retained for the existing
Twilio call sites.
- `enterprise/app/models/call.rb` — adds `default_ice_servers` (driven
by `VOICE_CALL_STUN_URLS` env), `direction_label` alias for the
`inbound`/`outbound` strings the FE expects, and
`ringing?`/`in_progress?`/`terminal?` predicates used throughout the
pipeline.

**Outgoing-channel guard**
- `app/services/base/send_on_channel_service.rb` — extends
`invalid_message?` to skip messages with `content_type == 'voice_call'`.
Without this, agent-initiated outbound calls (PR-4) would deliver
\"WhatsApp Call\" as a text message to the contact every time.

**Twilio call-site update**
- `enterprise/app/controllers/twilio/voice_controller.rb` — drops the
now-redundant `account: current_account` kwarg from the
`Voice::InboundCallBuilder.perform!` call.

**Tests**
- New: `spec/enterprise/services/whatsapp/incoming_call_service_spec.rb`
(5 examples — calling-disabled, inbound connect, outbound connect,
terminate completed, terminate no-answer, unknown event).
- New:
`spec/enterprise/services/whatsapp/call_permission_reply_service_spec.rb`
(3 examples — accept, reject, calling-disabled).
- Updated: `spec/enterprise/services/voice/inbound_call_builder_spec.rb`
and `spec/enterprise/controllers/twilio/voice_controller_spec.rb` to
drop the `account:` kwarg from call expectations.

## How to test

In `rails console` against an account with a WhatsApp inbox where
`provider_config['calling_enabled']` is true:

```ruby
inbox = Inbox.find(<id>)
params = { calls: [{ id: 'wacid_test', from: '15550001111', event: 'connect',
                     session: { sdp: 'v=0...', sdp_type: 'offer' } }] }
Whatsapp::IncomingCallService.new(inbox: inbox, params: params).perform
# => Conversation + Call (status: 'ringing', provider: 'whatsapp') + voice_call message bubble
# => ActionCable broadcasts `voice_call.incoming` to the assignee or account-wide

# Then terminate it:
Whatsapp::IncomingCallService.new(inbox: inbox,
  params: { calls: [{ id: 'wacid_test', event: 'terminate', duration: 0, terminate_reason: 'no_answer' }] }
).perform
# => Call status flips to 'no_answer', message bubble updates, `voice_call.ended` broadcast fires
```

End-to-end browser flow (Meta → cable → UI) requires the controller from
PR-4 and the frontend from PR-9.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:23:57 +05:30
Sojan JoseandGitHub 086aa36ffe feat(companies): add notes and history to company details (#14401) 2026-05-11 23:15:25 +05:30
f6be0d80ef feat: UI changes for document auto sync [AI-153] (#14258)
# Pull Request Template

## Description

FE code for document sync

Adds:
- UI to show counts (stats) of available web pages, stale and synced
documents and last synced at
- Bulk action and manual ways to sync web documents
- index to stats related columns

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

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

https://linear.app/chatwoot/issue/AI-153/fe-document-auto-sync

Documents dashboard:
<img width="2160" height="986" alt="CleanShot 2026-05-11 at 17 57 09@2x"
src="https://github.com/user-attachments/assets/6d934764-964c-4656-b005-1b4f0329e553"
/>

Filters:
<img width="1138" height="564" alt="CleanShot 2026-05-11 at 17 58 13@2x"
src="https://github.com/user-attachments/assets/cee780e6-eb8f-4aed-8cc5-b674244a821b"
/>

Needs update:
<img width="2222" height="966" alt="CleanShot 2026-05-11 at 17 57 53@2x"
src="https://github.com/user-attachments/assets/70c85ddd-7eb1-4328-ba14-7929e67e7b36"
/>

pdfs:
<img width="2180" height="558" alt="CleanShot 2026-05-11 at 17 58 30@2x"
src="https://github.com/user-attachments/assets/975b5c9f-bd1c-4979-9870-8f926d7f6e11"
/>

bulk actions:
<img width="2244" height="992" alt="CleanShot 2026-05-11 at 17 58 57@2x"
src="https://github.com/user-attachments/assets/bdb3c63f-d2de-41dc-a6d5-8821d3303be0"
/>

single url sync:
<img width="2264" height="722" alt="CleanShot 2026-05-11 at 17 59 19@2x"
src="https://github.com/user-attachments/assets/7d7323a5-0fcb-4be9-8635-55e56964999b"
/>



## Checklist:

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-11 20:13:29 +05:30
salmonumbrellaandGitHub fe44b07147 feat(companies): add company detail page (#14054) 2026-05-06 20:50:27 +05:30
0bd0cab868 feat(voice): Attach call recordings + show call duration on the bubble (#14344)
When an inbound voice call ends, the conversation bubble now (1) renders
an inline audio player as soon as Twilio finishes the recording and (2)
shows the call duration alongside "Call ended" so the agent gets the
at-a-glance summary without opening the recording.

Fixes
https://linear.app/chatwoot/issue/PLA-118/feat-recordings-on-calls-should-be-attached-on-the-conversation
and
https://linear.app/chatwoot/issue/PLA-119/duration-of-the-call-is-not-visible-on-the-chat-bubble

## How to test

1. Set up a Twilio voice inbox and trigger an inbound call.
2. Answer the call from an agent, talk for a few seconds, then hang up.
3. As soon as the call ends, the bubble should read **"Call ended —
0:NN"** (where NN is the call duration in seconds).
4. Wait a few seconds for Twilio to finish processing the recording
(usually <30s after hangup).
5. The same bubble should now show an inline audio player below the
duration. Press play; the recording should be audible.
6. Refresh the page — both the duration and the player should still be
there.
7. End a second call on the same conversation — its bubble should get
its own duration + player, independent of the first.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-04 17:14:01 +04:00
353089473e feat(voice): Assignment aware visibility and join conflict for inbound calls (#14333)
### Description
Inbound voice calls now route ownership cleanly: the call widget is
hidden from agents who aren't the conversation assignee, the first agent
to pick up becomes the assignee, and any later join attempt by another
agent is rejected with a clear "<agent> is already handling the call."
alert.

Closes
https://linear.app/chatwoot/issue/PLA-98/inbound-voice-calls-assignment-aware-visibility-auto-assignment-on

### How to test

1. As Agent A and Agent B, open the dashboard for the same voice inbox
in two browsers.
2. Place an inbound call to the inbox with the conversation
**unassigned** — both agents should see the call widget.
3. Have Agent A click **Join**. Agent A's widget transitions to the
active call; Agent B's widget disappears (conversation is now assigned
to Agent A).
4. While the call is in progress, attempt to join from a third agent
(e.g., via the bubble in the conversation timeline) — the join is
rejected with the toast `Agent A is already handling the call.`
5. Resolve the conversation, then place a second call to a conversation
that is already manually assigned to Agent A — only Agent A sees the
widget; nobody else does.
6. Race test: trigger two near-simultaneous join attempts (two agents
click Join within a few hundred ms of each other) — exactly one wins;
the other gets the conflict alert.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-30 18:38:10 +04:00
1124c1b4c2 feat(voice): Wire Twilio voice flow through unified call model (#14091)
Twilio voice now uses first-class `Call` records as the source of truth
for call state, instead of storing it on
`conversation.additional_attributes` and `conversation.identifier`. Each
call gets its own record, its own `voice_call` bubble matched by
`call_sid`, and its own conference name keyed off `Call.id`. Multiple
calls on the same conversation (for `lock_to_single_conversation`
inboxes) now work correctly, and the conversation card stays in sync
with the real latest message.
Fixes https://linear.app/chatwoot/issue/PLA-121/lock-to-single-thread

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-30 11:25:39 +04:00
Aakash BakhleandGitHub 568aae875b feat: wire up auto-sync job backend [AI-150] (#14117)
# Pull Request Template

## Description

- Wires up Controllers to auto-sync job
- adds plan based sync schedule
- a scheduler that runs every hour to check syncable documents
- guards the whole feature behind feature flag by reclaiming
`twilio_content_templates`
- Adds a global and account level cap on how many documents to enqueue
to prevent sudden burst at first run
- some refactor to simplify code
- specs

Fixes # (issue)

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

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

specs and locally

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-04-29 14:47:14 +05:30
0e122188e9 feat: Add voice calling as a capability on Twilio SMS channel(Enterprise) (#13963)
Voice calling is now a capability on the existing TwilioSms rather than
a separate Voice model. A single Twilio phone number handles both SMS
and voice calls through one inbox.
Fixes
https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel
and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms

**What changed**
- Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms
- Added voice_enabled, twiml_app_sid, api_key_secret columns to
channel_twilio_sms table
  - Dropped channel_voice table (no production data)
- All voice logic lives in Enterprise layer via
prepend_mod_with('Channel::TwilioSms')
- Added Voice settings tab on Twilio SMS inbox settings to
enable/disable voice
  - Validates Twilio number voice capability before provisioning
- Teardown service cleans up TwiML app and credentials when voice is
disabled
- Frontend voice detection uses isVoiceCallEnabled() /
getVoiceCallProvider() helpers — extensible to future providers
  - Gated by channel_voice feature flag

**How to test**

1. Enable feature flag:
Account.find(<id>).enable_features('channel_voice')
2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials →
verify incoming/outgoing calls and SMS work
3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox
→ Voice tab → toggle on → provide API key credentials → verify calls
work
4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted,
credentials cleared, SMS still works
5. Re-enable voice: Toggle on again → must provide api_key_secret again
→ new TwiML app provisioned

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-29 11:32:19 +04:00
PranavandGitHub 751c28d94d feat(ee): Add article translation via LLM in help center (#14136)
Adds the ability to translate help center articles to other languages using Captain's LLM infrastructure. Translated articles are created as drafts linked to the source article.

Fixes
https://linear.app/chatwoot/issue/CW-6901/translate-article-to-another-language

**How to test**

1. Navigate to Help Center → Articles for a portal with multiple locales
2. Click the three-dot menu on any article → "Translate"
3. Select a target language and category → click Translate
4. Switch to the target locale — the translated article appears as a
draft
5. Try translating the same article again — a warning shows the existing
translation with a link to open it in a new tab
6. Click "Overwrite and translate" to replace the existing translation


https://github.com/user-attachments/assets/1d2e991b-f0ac-403a-bcc1-2181b5731ea4
2026-04-24 08:51:26 -07:00
Shivam MishraandGitHub e5107604a0 feat: account enrichment using context.dev [UPM-27] (#13978)
## Account branding enrichment during signup

This PR does the following

### Replace Firecrawl with Context.dev

Switches the enterprise brand lookup from Firecrawl to Context.dev for
better data quality, built-in caching, and automatic filtering of
free/disposable email providers. The service interface changes from URL
to email input to match Context.dev's email endpoint. OSS still falls
back to basic HTML scraping with a normalized output shape across both
paths.

The enterprise path intentionally does not fall back to HTML scraping on
failure — speed matters more than completeness. We want the user on the
editable onboarding form fast, and a slow fallback scrape is worse than
letting them fill it in.

Requires `CONTEXT_DEV_API_KEY` in Super Admin → App Config. Without it,
falls back to OSS HTML scraping.

### Add job to enrich account details

After account creation, `Account::BrandingEnrichmentJob` looks up the
signup email and pre-fills the account name, colors, logos, social
links, and industry into `custom_attributes['brand_info']`.

The job signals completion via a short-lived Redis key (30s TTL) + an
ActionCable broadcast (`account.enrichment_completed`). The Redis key
lets the frontend distinguish "still running" from "finished with no
results."
2026-04-08 11:16:52 +05:30
Tanmay Deep SharmaandGitHub 6f5ad8f372 fix: strip manually_managed_features from params in super admin account create (#13983)
## Summary

When a Super Admin creates a new account via the Administrate dashboard,
the `manually_managed_features` field (a virtual attribute stored in
`internal_attributes` JSON) is passed to `Account.new(...)`, raising
`ActiveModel::UnknownAttributeError`. The existing `update` action
already strips this param — this fix adds the same handling to `create`.

Closes -> https://linear.app/chatwoot/issue/INF-66
Related Sentry ->
https://chatwoot-p3.sentry.io/issues/7168237533/?project=6382945&referrer=Linear

## Type of change

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

## How to reproduce

1. Log in as Super Admin
2. Navigate to Accounts → New
3. Fill in the form (with or without manually managed features selected)
4. Submit → `ActiveModel::UnknownAttributeError: unknown attribute
'manually_managed_features' for Account`

## What changed

- Added a `create` override in
`Enterprise::SuperAdmin::AccountsController` that strips
`manually_managed_features` from params before calling `super`, then
persists them via `InternalAttributesService` after the account is
saved.
2026-04-02 19:58:43 +05:30
8daf6cf6cb feat: captain custom tools v1 (#13890)
# Pull Request Template

## Description

Adds custom tool support to v1

## Type of change
- [x] New feature (non-breaking change which adds functionality)


## How Has This Been Tested?

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

<img width="1816" height="958" alt="CleanShot 2026-03-24 at 11 37 33@2x"
src="https://github.com/user-attachments/assets/2777a953-8b65-4a2d-88ec-39f395b3fb47"
/>

<img width="378" height="488" alt="CleanShot 2026-03-24 at 11 38 18@2x"
src="https://github.com/user-attachments/assets/f6973c99-efd0-40e4-90fe-4472a2f63cea"
/>

<img width="1884" height="1452" alt="CleanShot 2026-03-24 at 11 38
32@2x"
src="https://github.com/user-attachments/assets/9fba4fc4-0c33-46da-888a-52ec6bad6130"
/>



## Checklist:

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-04-02 12:40:11 +05:30
Sivin VargheseandGitHub 4517c50227 feat: support bulk select and delete for documents (#13907) 2026-03-26 19:48:12 +05:30
14df7b3bc1 fix: ai-assist 404 on CE (#13891)
# Pull Request Template

## Description

Relocate controller from enterprise/ to app/ and add
Api::V1::Accounts::Captain::TasksController.prepend_mod_with for EE
overrides.

Fixes: Ai assist giving 404 on CE

## Type of change

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

## How Has This Been Tested?

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

before:
<img width="482" height="130" alt="image"
src="https://github.com/user-attachments/assets/f51dc28a-ac54-45c4-9015-6f956fdf5057"
/>

after:
<img width="458" height="182" alt="image"
src="https://github.com/user-attachments/assets/eb86a679-5482-4157-9f4e-f3e9953d8649"
/>


## Checklist:

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

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-03-24 16:58:11 +05:30
Aakash BakhleandGitHub 290dd3abf5 feat: allow captain to access contact attributes (#13850)
# Pull Request Template

## Description

Captain v1 does not have access to contact attributes. Added a toggle to
let user choose if they want contact information available to Captain.

## Type of change

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

## How Has This Been Tested?

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

Specs and locally
<img width="1924" height="740" alt="CleanShot 2026-03-19 at 18 48 19@2x"
src="https://github.com/user-attachments/assets/353cfeaa-cd58-40eb-89e7-d660a1dc1185"
/>

![Uploading CleanShot 2026-03-19 at 18.53.26@2x.png…]()


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-03-20 16:15:06 +05:30
Shivam MishraandGitHub 550b408656 fix: restrict existing user sign-in to account members (#13793)
SAML sign-in now only links an existing user when that user already
belongs to the account that initiated SSO. New users can still be
created for SAML-enabled accounts, and invited members can continue to
sign in through their IdP, but SAML will no longer auto-attach an
unrelated existing user record during login.

**What changed**
- Added an account-membership check before SAML reuses an existing user
by email.
- Kept first-time SAML user creation unchanged for valid new users.
- Added builder and request specs covering the allowed and rejected
login paths.
2026-03-13 12:22:25 +05:30
Aakash BakhleandGitHub 87f5af4caa fix: playground captain v2 scenarios (#13747)
# Pull Request Template

## Description

Playground now uses v2. It was only wired to use v1. Traces get `source:
playground` on langfuse when playground has been used.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

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

locally and specs
<img width="1806" height="1276" alt="image"
src="https://github.com/user-attachments/assets/41ef4eb3-52b1-4b8e-9a4f-e8510c90cb39"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-03-11 14:05:16 +05:30
Sojan JoseandGitHub 2ab117e8eb feat(cloud-billing): cancel subscriptions at period end on deletion mark (#13580)
## How to reproduce
In Chatwoot Cloud, mark an account for deletion from account settings
while the account has an active Stripe subscription. Before this change,
deletion marking did not explicitly mark subscriptions to stop renewing
at period end.

## What changed
This PR adds `Enterprise::Billing::CancelCloudSubscriptionsService` and
calls it from the delete action path in
`Enterprise::Api::V1::AccountsController`. The service lists only active
Stripe subscriptions for the customer and sets `cancel_at_period_end:
true` when needed. The account deletion schedule remains unchanged
(existing static 7-day behavior), and Stripe deleted-event fallback
behavior remains unchanged.

## How this was tested
Added and updated specs:
-
`spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
-
`spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb`

Executed:
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
- `bundle exec rspec
spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb:363`
2026-02-19 11:40:06 +05:30
04b2901e1f feat: Conversation workflows(EE) (#13040)
We are expanding Chatwoot’s automation capabilities by
introducing **Conversation Workflows**, a dedicated section in settings
where teams can configure rules that govern how conversations are closed
and what information agents must fill before resolving. This feature
helps teams enforce data consistency, collect structured resolution
information, and ensure downstream reporting is accurate.

Instead of having auto‑resolution buried inside Account Settings, we
introduced a new sidebar item:
- Auto‑resolve conversations (existing behaviour)
- Required attributes on resolution (new)

This groups all conversation‑closing logic into a single place.

#### Required Attributes on Resolve

Admins can now pick which custom conversation attributes must be filled
before an agent can resolve a conversation.

**How it works**

- Admin selects one or more attributes from the list of existing
conversation level custom attributes.
- These selected attributes become mandatory during resolution.
- List all the attributes configured via Required Attributes (Text,
Number, Link, Date, List, Checkbox)
- When an agent clicks Resolve Conversation:
If attributes already have values → the conversation resolves normally.
If attributes are missing → a modal appears prompting the agent to fill
them.

<img width="1554" height="1282" alt="CleanShot 2025-12-10 at 11 42
23@2x"
src="https://github.com/user-attachments/assets/4cd5d6e1-abe8-4999-accd-d4a08913b373"
/>


#### Custom Attributes Integration

On the Custom Attributes page, we will surfaced indicators showing how
each attribute is being used.

Each attribute will show badges such as:

- Resolution → used in the required‑on‑resolve workflow

- Pre‑chat form → already existing

<img width="2390" height="1822" alt="CleanShot 2025-12-10 at 11 43
42@2x"
src="https://github.com/user-attachments/assets/b92a6eb7-7f6c-40e6-bf23-6a5310f2d9c5"
/>


#### Admin Flow

- Navigate to Settings → Conversation Workflows.
- Under Required attributes on resolve, click Add Required Attribute.
- Pick from the dropdown list of conversation attributes.
- Save changes.

Agents will now be prompted automatically whenever they resolve.

<img width="2434" height="872" alt="CleanShot 2025-12-10 at 11 44 42@2x"
src="https://github.com/user-attachments/assets/632fc0e5-767c-4a1c-8cf4-ffe3d058d319"
/>



#### NOTES
- The Required Attributes on Resolve modal should only appear when
values are missing.
- Required attributes must block the resolution action until satisfied.
- Bulk‑resolve actions should follow the same rules — any conversation
missing attributes cannot be bulk‑resolved, rest will be resolved, show
a notification that the resolution cannot be done.
- API resolution does not respect the attributes.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2026-01-27 11:36:20 +04:00
6a482926b4 feat: new Captain Editor (#13235)
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
2026-01-21 13:39:07 +05:30
PranavandGitHub a8b302d4cd feat(ee): Review Notes for CSAT Reports (#13289)
CSAT scores are helpful, but on their own they rarely tell the full
story. A drop in rating can come from delayed timelines, unclear
expectations, or simple misunderstandings, even when the issue itself
was handled correctly.

Review Notes for CSAT let admins/report manager roles add internal-only
context next to each CSAT response. This makes it easier to interpret
scores properly and focus on patterns and root causes, not just numbers.


<img width="2170" height="1680" alt="image"
src="https://github.com/user-attachments/assets/56df7fab-d0a7-4a94-95b9-e4c459ad33d5"
/>


### Why this matters

* Capture the real context behind individual CSAT ratings
* Clarify whether a low score points to a genuine service issue or a
process gap
* Spot recurring themes across conversations and teams
* Make CSAT reviews more useful for leadership reviews and
retrospectives

### How Review Notes work

**View CSAT responses**
Open the CSAT report to see overall metrics, rating distribution, and
individual responses.

**Add a Review Note**
For any CSAT entry, managers can add a Review Note directly below the
customer’s feedback.

**Document internal insights**
Use Review Notes to capture things like:

* Why a score was lower or higher than expected
* Patterns you are seeing across similar cases
* Observations around communication, timelines, or customer expectations

Review Notes are visible only to administrators and people with report
access only. We may expand visibility to agents in the future based on
feedback. However, customers never see them.

Each note clearly shows who added it and when, making it easy to review
context and changes over time.
2026-01-15 19:53:57 -08:00
005a22fd69 feat: Add sorting by contacts count to companies list (#13012)
## Description

Adds the ability to sort companies by the number of contacts they have
(contacts_count) in ascending or descending order. This is part of the
Chatwoot 5.0 release requirements for the companies feature.

The implementation uses a scope-based approach consistent with other
sorting implementations in the codebase (e.g., contacts sorting by
last_activity_at).

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Available Sorting Options

After this change, the Companies API supports the following sorting
options:

| Sort Field | Type | Ascending | Descending |
|------------|------|-----------|------------|
| `name` | string | `?sort=name` | `?sort=-name` |
| `domain` | string | `?sort=domain` | `?sort=-domain` |
| `created_at` | datetime | `?sort=created_at` | `?sort=-created_at` |
| `contacts_count` | integer (scope) | `?sort=contacts_count` |
`?sort=-contacts_count` |

**Note:** Prefix with `-` for descending order. Companies with NULL
contacts_count will appear last (NULLS LAST).

## CURL Examples

**Sort by contacts count (ascending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=contacts_count' \
  -H 'api_access_token: YOUR_API_TOKEN'
```

**Sort by contacts count (descending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-contacts_count' \
  -H 'api_access_token: YOUR_API_TOKEN'
```

**Sort by name (ascending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=name' \
  -H 'api_access_token: YOUR_API_TOKEN'
```

**Sort by created_at (descending):**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-created_at' \
  -H 'api_access_token: YOUR_API_TOKEN'
```

**With pagination:**
```bash
curl -X GET 'https://app.chatwoot.com/api/v1/accounts/{account_id}/companies?sort=-contacts_count&page=2' \
  -H 'api_access_token: YOUR_API_TOKEN'
```

## How Has This Been Tested?

- Added RSpec tests for both ascending and descending sort
- All 24 existing specs pass
- Manually tested the sorting functionality with test data

**Test configuration:**
- Ruby 3.4.4
- Rails 7.1.5.2
- PostgreSQL (test database)

**To reproduce:**
1. Run `bundle exec rspec
spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`
2. All tests should pass (24 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

## Technical Details

**Backend changes:**
- Controller: Added `sort_on :contacts_count` with scope-based sorting
- Model: Added `order_on_contacts_count` scope using
`Arel::Nodes::SqlLiteral` and `sanitize_sql_for_order` with `NULLS LAST`
for consistent NULL handling
- Specs: Added 2 new tests for ascending/descending sort validation

**Files changed:**
- `enterprise/app/controllers/api/v1/accounts/companies_controller.rb`
- `enterprise/app/models/company.rb`
-
`spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb`

**Note:** This PR only includes the backend implementation. Frontend
changes (sort menu UI + i18n) will follow in a separate commit.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2026-01-09 15:20:08 -08:00
PranavandGitHub 837026c146 feat: Use amplitude for Cloud Analytics (#13217)
Migrates our analytics integration on Cloud from PostHog to Amplitude.
This change updates the core AnalyticsHelper class to use the Amplitude
SDK while maintaining the same tracking interface. Rest of all existing
analytics calls throughout the codebase continue to work without
modification.

**Changes:**
- Replace PostHog analytics with Amplitude SDK
- Rename ANALYTICS_TOKEN to CLOUD_ANALYTICS_TOKEN for clarity
- Fix bug in page() method signature that was causing malformed payloads
2026-01-09 09:32:09 -08:00
Sojan JoseandGitHub d2ba9a2ad3 feat(enterprise): add voice conference API (#13064)
The backend APIs for the voice call channel 
ref: #11602
2025-12-15 15:11:59 -08:00
Aakash BakhleandGitHub 1de8d3e56d feat: legacy features to ruby llm (#12994) 2025-12-11 14:17:28 +05:30
Muhsin KelothandGitHub 20fa5eeaa5 fix: Prevent SLA deletion timeouts by moving to async job (#12944)
This PR fixes the HTTP 500 timeout errors occurring when deleting SLA
policies that have large volumes of historical data.
The fix moves the deletion workflow to asynchronous background
processing using the existing `DeleteObjectJob`.
By offloading heavy cascaded deletions (applied SLAs, SLA events,
conversation nullifications) from the request cycle, the API can now
return immediately while the cleanup continues in the background
avoiding the `Rack::Timeout::RequestTimeoutException`. This ensures that
SLA policies can be deleted reliably, regardless of data size.


### Problem
Deleting an SLA policy via `DELETE
/api/v1/accounts/{account_id}/sla_policies/{id}` fails consistently with
`Rack::Timeout::RequestTimeoutException (15s)` for policies with large
amounts of related data.

Because the current implementation performs all dependent deletions
**synchronously**, Rails processes:

- `has_many :applied_slas, dependent: :destroy` (thousands)
- Each `AppliedSla#destroy` → triggers destruction of many `SlaEvent`
records
- `has_many :conversations, dependent: :nullify` (thousands)

This processing far exceeds the Rack timeout window and consistently
triggers HTTP 500 errors for users.

### Solution

This PR applies the same pattern used successfully in Inbox deletion.

**Move deletion to async background jobs**

- Uses `DeleteObjectJob` for centralized, reliable cleanup.
- Allows the DELETE API call to respond immediately.

**Chunk large datasets**

- Records are processed in **batches of 5,000** to reduce DB load and
avoid job timeouts.
2025-12-10 12:28:47 +05:30