Skip to content

Show who is handling a chat, and let humans take the AI out - #305

Merged
chattermate merged 5 commits into
mainfrom
feature/inbox-handler-human-routing
Aug 20, 2026
Merged

Show who is handling a chat, and let humans take the AI out#305
chattermate merged 5 commits into
mainfrom
feature/inbox-handler-human-routing

Conversation

@chattermate

@chattermate chattermate commented Aug 13, 2026

Copy link
Copy Markdown
Owner

The inbox showed the AI agent's name on every row and a bare OPEN/CLOSED pill, so you couldn't tell an AI-handled chat from one a colleague had taken over — the list query never returned the assignee. And the only way to stop the AI answering was to claim the chat yourself.

Inbox

  • The chat list API now returns user_id/user_name
  • Each row gets a badge: AI / Waiting for human / the agent's name
  • The list, chat header and info panel now share one chatHandler() instead of three copies

Routing to a human

  • POST /sessions/{id}/route-to-human — queues a chat for the team without claiming it. Button in the chat footer and the info panel. This one is a real transfer, so it reuses TRANSFERRED with no assignee, the state every AI path already checks.
  • New per-agent "Let AI Answer" toggle. A human-only agent is not a transfer — the chat was never with the AI, so there is nothing to hand over. Its messages skip every automated branch and fall through to the same store-and-relay a claimed chat takes: no "I'm transferring you" line, no handoff contact form. The visitor gets one plain acknowledgement, notify_new_chat tells the team, and the row reads "Waiting for human".

Widget honesty

Two things the widget was asserting without evidence:

  • The typing indicator. It switched on whenever the visitor sent something and only chat_response switched it off, so every state that produces no reply left it spinning. The backend now emits bot_typing immediately before it actually runs the workflow, the model or the availability agent, and that is the only thing that turns it on — the no-reply cases are correct by construction. Cleared on disconnect and after a 60s timeout.
  • The header. "Online · replies instantly" was hardcoded. It now follows who has to reply: unchanged while the AI answers, business-hours-based when a person does ("usually replies in a few minutes" / "we'll reply when we're back"). The hours check moved out of transfer_agent into app/utils/business_hours so the header and the AI's transfer wording share one definition.

Availability is business hours only — is_online has no heartbeat, so it reports people who closed a tab days ago as present.

One migration: agents.ai_replies_enabled, defaults true, so existing agents are unchanged. backend/assets/widget.js is rebuilt (build:widget:prod), since it is checked in.

Not covered: the inbox still doesn't live-update when another agent takes a chat over. It never has for any field — agents can only join session rooms they own, so there's no channel for it. Needs an org-wide room with permission filtering; worth doing separately. There is also still no typing signal in the other direction: the visitor sees nothing while a human is typing.

@chattermate
chattermate force-pushed the feature/inbox-handler-human-routing branch from 3005d7e to 23e9189 Compare August 13, 2026 11:10
Every inbox row showed the AI agent's name and a bare OPEN/CLOSED pill,
so there was no way to tell an AI-handled chat from one a colleague had
taken over — the list query never returned the assignee. And the only way
to stop the AI answering was to claim the chat yourself.

- return user_id/user_name from the chat overview API
- badge each row: AI / Waiting for human / the agent's name
- derive the list, chat header and info panel from one chatHandler()
- POST /sessions/{id}/route-to-human queues a chat for the team without
  claiming it
- per-agent "Let AI Answer" toggle for teams that want no AI at all

A human-only agent is not a transfer: the chat was never with the AI, so
there is nothing to hand over. Its messages skip every automated branch
and fall through to the same store-and-relay a claimed chat takes, and
notify_new_chat tells the team it is waiting. The row reads "Waiting for
human" from the agent's own flag.

The explicit per-chat hand-off does use TRANSFERRED with no assignee,
which every AI path already checks, so no new status was needed.

Signed-off-by: chattermate <admin@chattermate.chat>
@chattermate
chattermate force-pushed the feature/inbox-handler-human-routing branch from 23e9189 to ce8a9a1 Compare August 13, 2026 11:36
The widget switched the dots on whenever the visitor sent something and
waited for chat_response to switch them off. Any state that produces no
bot reply — AI switched off on the agent, a human handling the chat, a
queued chat, a message-limit refusal — left them spinning forever, and
each one needed its own client-side guard to suppress. Three rounds of
that is enough.

The backend already knows what will happen to a message the moment it
arrives, so it now says so: bot_typing is emitted immediately before the
workflow, the model or the availability agent runs, and nothing else
turns the indicator on. The no-reply cases are correct by construction
rather than by another guard.

- emit bot_typing from the three branches that actually produce a reply
- widget listens for it; sendMessage no longer decides anything
- clear on disconnect, and after a 60s safety timeout so a backend that
  dies mid-reply cannot strand the dots
- drop the aiRepliesEnabled hint from __INITIAL_DATA__, now redundant

Rebuilds backend/assets/widget.js, which is checked in — the previous
commit changed the widget source without it, so none of those fixes were
reaching anyone.

Signed-off-by: chattermate <admin@chattermate.chat>
The header said "Online · replies instantly" unconditionally — at 3am,
with everyone logged off, on an agent whose AI never answers. Nothing
about availability was even sent to the widget, so it could not have said
anything else.

It now reflects who has to reply. While the AI answers, instantly is true
around the clock and the line is unchanged. When a person has to answer —
a human-only agent, or a chat someone has taken over — the line follows
the organization's business hours: "usually replies in a few minutes"
while open, "we'll reply when we're back" with a muted dot outside.

Availability is business hours alone. is_online is set at login and
cleared only on logout or a manual toggle, with no heartbeat behind it,
so it reports people who closed a tab days ago as present.

The hours check moves out of transfer_agent into app/utils/business_hours
so the presence line and the AI's transfer wording cannot contradict each
other, and so answering "are you open?" no longer needs a model call.

Signed-off-by: chattermate <admin@chattermate.chat>
cleanup() and reconnect both removeAllListeners before dropping the
socket, so the disconnect handler that would have cleared the timer never
runs. A pending one outlived the widget and could fire into the next
session.

Also stops window.d.ts redeclaring the presence shape — it now references
WidgetPresence, so the two cannot drift.

Signed-off-by: chattermate <admin@chattermate.chat>
It was never meant to be tracked — an untracked artifact from a local
:dev build that a `git add -A` swept into the inbox commit. Only the :dev
webclient build emits a sourcemap; the committed loader is the :prod one
and carries no sourceMappingURL, so nothing referenced the 104K file.

Ignored so it cannot come back the same way.

Signed-off-by: chattermate <admin@chattermate.chat>
@chattermate
chattermate merged commit 9acbfcf into main Aug 20, 2026
7 checks passed
@chattermate
chattermate deleted the feature/inbox-handler-human-routing branch August 20, 2026 05:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant