Show who is handling a chat, and let humans take the AI out - #305
Merged
Conversation
chattermate
force-pushed
the
feature/inbox-handler-human-routing
branch
from
August 13, 2026 11:10
3005d7e to
23e9189
Compare
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
force-pushed
the
feature/inbox-handler-human-routing
branch
from
August 13, 2026 11:36
23e9189 to
ce8a9a1
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
user_id/user_namechatHandler()instead of three copiesRouting 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 reusesTRANSFERREDwith no assignee, the state every AI path already checks.notify_new_chattells the team, and the row reads "Waiting for human".Widget honesty
Two things the widget was asserting without evidence:
chat_responseswitched it off, so every state that produces no reply left it spinning. The backend now emitsbot_typingimmediately 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.transfer_agentintoapp/utils/business_hoursso the header and the AI's transfer wording share one definition.Availability is business hours only —
is_onlinehas 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.jsis 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.