Feature docs

Chat

TECHNOLOGY

Dealerships run website chat and Facebook/Instagram DMs through a third-party AI today. We are replacing it. Chat becomes a real channel in the CRM — pink, the one accent pair with no other meaning — beside Text, Email, Note, Log and Call in the composer, feed and row Inbox already owns.

Three things change. Every turn is its own message in the feed, one bubble each, rather than a batch of internal notes dumped after the fact. A rep steps into a thread the assistant is handling by switching the DAISI pill to Assist. And the conversation starts before the customer exists: a nameless website visitor chats, the AI holds the thread, and the whole transcript lands on the record when a phone number arrives.

Seven days on the incumbent at our largest affected dealership: 8,413 messages across 637 conversations — 48% handled by the assistant alone, 48% by the assistant and a human, 5% human only. Half of all conversations involve a human stepping in, so a rep must always be able to tell which turns were theirs. Chat is a follow-up to Inbox, which is in development now — it reuses the same composer, feed, row and header, so the two ship in sequence.

Rulings ledger

DateRuling
08-14Chat is pink — the one accent pair with no assigned meaning; no token was added.
08-14Sub-channels are context, not tabs — ONE Chat tab; Website/Facebook/Instagram is a property of the conversation, displayed, never a picker (you cannot start a Facebook DM outbound, so a chooser would lie).
08-14One bubble per turn, not one per session — individual activity items are the point; consecutive turns from the same actor group under one stamp.
08-14Chat joins the unread badge — amending the 08-06 ruling that scoped it to call, SMS and email; the badge answers "who is waiting on me", and a waiting chat qualifies.
08-14There is no takeover feature — a rep who wants the conversation switches the existing DAISI mode pill to Assist. No new control, no in-feed marker, nothing built.
08-14AI and human turns stay visually distinguishable inside one thread — a change of actor always breaks a group.
08-14The mark is fa-comments — both single-bubble marks are already spent on SMS.
08-14The sub-channel is not shown at list density — the row has one icon slot and shows the class of sender.
08-14Anonymous turns are never back-filled with a name once identity is known — the transcript records what was known at the time.
08-17Online status is website-only — we host the chat widget, so we know the visitor has the page open; Meta's Conversations API returns conversation and message metadata only — there is no presence signal to read — so the cell is hidden entirely on Facebook and Instagram rather than faked.
08-17Chat is inbound-initiated only — a rep cannot start a chat, so the Chat tab is disabled when no open thread exists.
08-17The channel is chosen the way a Log type is chosen — the composer carries a channel cell that defaults to the customer's last inbound chat channel and only offers channels with an open thread.
08-17Grouped turns keep their bubble header — only the stamp collapses; stripping the header broke the feed's pattern.
08-17A grouped turn is a feed item, not a parallel atom — it composes as .sb-activity plus a modifier, so it inherits every feed override for free.
08-17No reply-window countdown and no takeover marker — both removed as invented requirements.
09-02Opening a conversation marks it read, dealership-wide — one shared timestamp on the customer; the first opener clears it for the whole team. Chat threads follow the same model since chat counts in the unread badge.

Anatomy map

ComposerThe pink Chat channel, availability states, the seven-tab strip. Activity feedChat bubbles, grouping, authorship, the identification marker, anonymous turns. Conversation rowThe fa-comments row at list density — the class of sender, no sub-channel. Header stripThe conversation header above the feed. Mobile conversationThe customer screen a row opens into — there is no separate mobile conversation view.

The composer

Chat is a seventh channel in the one composer, with an availability model no other channel has.

Chat is channel #7, in pink — .sb-composer--chat{--cp-body:var(--pink-02)} + .cp-tab--chat.on{color:var(--pink-01);border-color:var(--pink-01)}, exactly parallel to the other six. Pink is the one accent pair with no assigned meaning, so it costs no existing semantic. The mark is fa-comments (two overlapping bubbles): fa-comment-alt-dots is taken by comm.text (SMS) and fa-comment-dots is the Text-Sent activity icon, so either single-bubble form reads as SMS — the plural reads as a live two-party conversation, which is the distinction a rep needs at a glance. Chat sits directly after Text, beside its messaging siblings rather than appended after Call. The tab strip stays neutral grey — only the body region tints — and there is still ONE universal blue send; no chat-specific send button. Seven-tab observation: at the narrow breakpoints with the widest DAISI pill (Assist + countdown) showing, the staged collapse still holds — labels shed to icon-only at the existing stage and the strip does not wrap, because the container query sheds the whole label set at once rather than per-tab. Nothing was retuned.
6 · Chat availability — available / can't initiate / visitor gone
a · Available — Facebook chat with an open thread, normal compose
Reply in the chat thread…
b · Can't initiate — no open chat thread; chat is inbound-initiated only, so the Chat tab is disabled
Compose a message or use a Template
c · Visitor gone — website chat, tab closed; no destination for a reply
Visitor left the site. A website chat has nowhere to deliver once the tab is closed — reach them on another channel, or leave a Note.
Two availability states no other channel has. Chat is inbound-initiated only — a rep cannot start a thread, so when none is open the Chat tab takes the .is-disabled treatment (permanent-for-now: nothing the rep does opens it; the customer must message first). Visitor gone (website) is different — a missing destination, not an opt-out: the tab stays live and the body carries a notice, distinguished by icon (fa-comment-slash) and copy, because a website chat has nowhere to deliver once the tab is closed but the rep can still reach the customer another way or leave a Note.

The feed

A conversation is a run of turns, grouped by actor and marked where identity arrives.

Chat in the feed is pink. The bubble and indicator variants in this atom family are colour-keyed, not channel-keyed — there is no .sb-bubble--text — so Chat follows the convention exactly with .sb-bubble--pink + .sb-ind--pink; a channel-keyed name would start a second naming system inside one family. Title grammar matches its siblings: Chat Sent / Chat Received, mark fa-comments. The sub-channel word lives in the existing .sub slot — the same slot SMS uses for “SMS” and AI-sent messages use for “Auto” — so Website / Facebook / Instagram needs no new markup. Inbound sits left with the indicator leading, outbound sits right with it trailing, and Show/Hide is unchanged.
Consecutive turns from the same actor within a short window collapse to one stamp plus stacked bubbles (Chat runs dozens of short turns per conversation). The grouping window is per-actor. Implemented as a WRAPPER (.sb-turngroup) plus a condensed head modifier (.sb-stamp--grouped, height only). A grouped turn IS a feed item — .sb-activity.sb-turngroup, never a parallel atom — so it inherits every existing and future .sb-activity override (feed width and the rest) for free, with no page-local rule and no allowlist. The modifier class carries only what genuinely differs: the tighter stacked-bubble spacing — the six base .sb-stamp rules are byte-unchanged, so the customer-profile feed that shares the atom is untouched.
Authorship extends the existing precedent, it does not replace it: the bubble stays the channel colour (pink) and the stamp row carries the DAISI glyph + actor name — AI messages are never tinted yellow. This is load-bearing, not cosmetic: 83% of the AI's messages are currently miscounted as human activity, and the GM's daily question is "what's the last thing a human did versus AI?". Hard constraint: a change of actor ALWAYS forces a new stamp — grouping never merges an AI turn with a human turn, which is what keeps authorship legible when turns collapse.
.sb-feedmark is the feed's non-message row — composed the same way, .sb-activity.sb-feedmark, so it spans the feed column wherever the feed is widened — a quiet boundary, not a bubble: no channel colour, hairline rules + gray type only, so it never competes with the messages around it. It carries a short line plus an optional small icon. Its one use is the identification boundary in a chat that began before the customer existed: "Identified as Marcus Delgado — phone collected" marks the turn where an anonymous website visitor became a customer record, connecting the anonymous half of the transcript to the named half.
A chat can begin before the customer exists — a nameless website visitor chats, the AI holds the thread, and only when a phone number is collected does the customer record get created and the whole transcript (anonymous part included) land on their timeline. Pre-identification turns render in the existing stamp with a neutral glyph (fa-globe) and the honest label "Website Visitor" — deliberately the same web-visitor vocabulary as the Website Session bubble (fa-globe, unnamed visitor), so an anonymous chat reads as its sibling in the same feed, not a foreign object. Consecutive anonymous turns group normally (one actor). Never back-filled: earlier turns keep the "Website Visitor" label permanently even after identity is known — the transcript records what was known at the time, and renaming them to the customer would misrepresent it (and imply the rep knew who they were talking to when they did not). The .sb-feedmark identification line ("Identified as Marcus Delgado — phone collected") is the third reserved use of the marker and is what connects the two halves. Do not "improve" this by back-filling names.

The row & list density

How a conversation reads in the thread list — one icon slot carrying the class of sender.

Chat rows carry the fa-comments mark in the single icon slot — the plural bubble form, because both single-bubble marks (fa-comment-alt-dots, fa-comment-dots) already read as SMS. The sub-channel is NOT shown at list density (08-06 ruling). The row has exactly one icon slot, one generalised actor word and one direction arrow, and it shows the class of sender because that is what triage needs; Website vs Facebook vs Instagram is revealed in the conversation header and on the bubble, never in the row.

The channel & presence row

Chat carries its sub-channel in the composer row, and on website chat the visitor's live presence.

The Chat body reuses the Log channel's .cp-logrow / .lg-cell row — no new atom. First cell is the sub-channel (pink, chevron): Website / Facebook / Instagram. It is not a free chooser — it defaults to the channel of the customer's last inbound chat message and only offers channels where an open thread exists. The second cell carries the live insight, channel-honest: for Website we host the widget, so we genuinely know presence — green dot "On the page now", or "Left the page · 2:14 PM". For Facebook/Instagram Meta's API is webhook/event based with no presence signal, so the presence cell is hidden entirely — not greyed, not faked with a stamp. The channel cell renders alone.

Mobile

Chat is the channel most likely to be used on a phone, so every state has a mobile answer.

On mobile the conversation IS this screen. Tapping a row in the Inbox list navigates into the customer; backing out returns to the list. There is no middle pane and no separate conversation screen — a mobile-only conversation view would be a fork of a screen that already ships. The composer belongs to this screen, and the DAISI mode pill lives in the composer's .cp-ai slot, never in the page header. .sb-cushdr is the mobile customer header and the only one. Chat renders here like any other channel: the pink Chat tab and channel row in the composer, pink bubbles in the feed with the sub-channel in .sub, and grouped turns as .sb-activity.sb-turngroup.

Engineering scope

Chat instances the existing UI atoms, but most of the channel behind them is new.

Open questions — two Inbox rulings Chat contradicts:

Full decision ledger: autopilot/CHAT_PLAN.md (repo). Composed under the Atomic documentation LAW: narrative + rulings ledger + anatomy map + @dsEmbed transclusions (build inlines them with provenance links) + eng scope notes.