Visitors who hit a wall with the AI agent can ask for a human and actually get one — in real time, using the same Conversations menu your team already uses to read transcripts.
ticket_escalation
capability (every preset ships with it enabled by default).
HumanIntentDetector matches against ~40
phrases ("talk to a human", "connect me to an
agent", "I need to speak with someone", etc.).
Typing one of those short-circuits the LLM and goes straight
to the handoff flow.
human_requested_at
timestamp on the server.
HumanRequestedEvent broadcasts to
the workspace's Reverb private channel so dashboard
tabs update in real time.
NotifyOperatorsHumanRequestedJob fans out
database + mail notifications to every workspace
member with live_chat_available=true.
claimed_by_user_id, claimed_at,
human_requested_at, and the operator-typing window,
then posts a one-line system message to the chat
("The operator has stepped away. The AI assistant is back to
help."). The visitor's "Connecting you with someone…" banner
disappears within the next poll; the conversation history stays
intact for analytics + future reference, and the AI resumes
on the next visitor message.
A few knobs live in Settings on the workspace level:
Two cascades fire when a visitor asks for a human:
HumanRequestedEvent broadcast on
the workspace's Reverb private channel. Every open dashboard
tab picks it up and updates the Conversations sidebar badge +
plays a sonner toast in real time.
NotifyOperatorsHumanRequestedJob
— queued database + mail notification (HumanRequestedNotification)
fanned out to every workspace member with
users.live_chat_available = true. Members who
had the dashboard closed still get an email so they can claim
the conversation on next sign-in.
If a captured lead also fires, the lead-captured email + dashboard toast cascade runs in addition. The handoff notification is idempotent: a second click within the 2-minute waiting window does not re-broadcast or re-notify.
The server picks one of three responses based on operator availability and your business hours when a visitor asks for a human:
RequestHumanController::WAIT_TIMEOUT_SECONDS = 120).
Each workspace member sets their availability individually in Profile settings:
Configure in Settings → Live chat. JSON-edited for now (a visual grid editor lands in the next release):
{
"enabled": true,
"timezone": "America/New_York",
"schedule": {
"monday": [{"start": "09:00", "end": "17:00"}],
"tuesday": [{"start": "09:00", "end": "17:00"}],
"wednesday": [
{"start": "09:00", "end": "12:00"},
{"start": "13:00", "end": "17:00"}
],
"thursday": [{"start": "09:00", "end": "17:00"}],
"friday": [{"start": "09:00", "end": "17:00"}],
"saturday": [],
"sunday": []
}
}
HH:mm. Empty array = closed all day."enabled": false (or leave the whole field blank) to stay always-on.In Settings → Live chat, paste an incoming-webhook URL for either platform. When a visitor asks for a human, a queued listener fires a compact ping with the conversation URL so an operator can jump straight in from Slack / Teams without opening the dashboard.
incoming-webhook URL from
the Slack app config. Slack auto-unfurls the conversation link.
Visitors should never sit on a "Connecting you…" bubble forever.
The widget runs a client-side 2-minute timer the moment the
handoff is requested (see RequestHumanController::WAIT_TIMEOUT_SECONDS).
When the timer fires without a claim, the banner flips to the
"No one's around" copy and the lead form opens so the visitor
can leave their email. The conversation row stays flagged on
the server so the operator can still claim later — the
notification cascade fires once on the initial request, so
operators see the queued conversation in the Conversations
sidebar regardless of whether the visitor stayed on the page.
Save the replies your team types over and over (password reset instructions, refund policy, shipping ETAs) at Settings → Canned replies. Each entry has a short label (what operators search by) and the full reply text. Reorder with the up/down handles — most-used replies should sit at the top.
In any live conversation, click the Canned reply button above the textarea. Fuzzy-search the label or content, pick one, and the textarea fills in. The operator can edit before hitting Send.
Toggle the reply box from Reply to Internal note (the textarea turns amber). Internal notes are visible to other operators in the conversation thread but never sent to the visitor. Useful for handoff context: "Visitor seems frustrated — I tried X already, please pick up." Auto-generated transfer audit messages also use this role.
Both directions, no setup required:
Implemented as 5-second self-expiring server-side timestamps; both sides poll the existing endpoints, so no extra infrastructure is needed.
Click the Transfer button in the reply bar. Online teammates surface at the top with a green "Online" badge; offline teammates are still listed (you may want to hand off to someone who'll claim later). Picking a target reassigns the claim, broadcasts to the visitor's widget so the "joined the chat" banner refreshes to the new operator's name, and drops a system note in the thread for context.
The Phase 2 JSON textarea is replaced by a visual 7-day grid at Settings → Live chat. Click the "+ Window" button on any day to add another open block (lunch break, split shifts), or check "Closed" to take that day off. Timezone picker has the 15 most common IANA zones plus a "Custom…" option for any zone PHP recognizes.
Categorize conversations so your team can filter and report on them. Manage the list at Settings → Tags: each tag has a label (max 60 chars) and a hex color that drives the chip background.
In any conversation thread, the end-side panel has a Tags section. Click a chip's × to detach; + Tag opens a fuzzy-search dropdown of unselected workspace tags.
Conversations list: a "Tags" select dropdown joins the existing Needs human / Live now filter pills. Each row also surfaces applied tags as compact chips alongside the existing badges.
After an operator releases the conversation, the visitor sees a "Was this helpful?" prompt with thumbs up / down buttons + an optional comment field. The first rating is locked server-side; later submissions update the comment only — buyers complained about Intercom-style "rating overwritten" surprises.
Operator side: the end-pane context panel surfaces the rating + comment under the visitor card. The Conversations list also shows a 👍 / 👎 chip on each row when a rating exists.
The thread page is now a true help-desk surface:
POST /app/conversations/{id}/note — internal notePOST /app/conversations/{id}/typing — operator typing hintPOST /app/conversations/{id}/transfer — reassign claimPOST / DELETE /app/conversations/{id}/tags/{tagId} — attach / detach tagPOST /api/v1/widget/typing — visitor typing hint (JWT)POST /api/v1/widget/satisfaction — visitor rating (JWT)GET / POST / PATCH / DELETE /app/settings/canned-replies/… — CRUD + reorderGET / POST / PATCH / DELETE /app/settings/tags/… — workspace tag CRUD