Releases: CleanSlice/bridle
SDK v0.13.3 — Archive transcript on New chat
Closes the loop on the New chat UX.
Before
New chat cleared the local panel and reconnected with a fresh anonId. But:
- token-flow kept the same
clientId→ server-side transcript was untouched. v0.13.2 patched this with a one-shotskipNextTranscriptflag so the replay was suppressed, but history just piled up on the server with no way to roll it over. - public flow with stable
anonIdhad the same long-term problem once hub started honoringanonId(planned).
Now
The SDK calls POST /api/agent/<id>/transcript/archive?channel=<clientId> right before the reconnect. Best-effort: if the endpoint isn't there (older hub) or the request fails, we still clear locally and skipNextTranscript keeps the replay from undoing it. Net effect: New chat is consistent regardless of which hub version the embed talks to.
Hub-side implementations
Bridle hub (bridle/nestjs/): new IBridleTranscriptGateway.archive() method on the persistence contract, default falls back to delete(). Integrators override to do the right thing for their storage (rename to timestamped sibling, move to cold tier, etc.). New endpoint POST :agentId/transcript/archive returns { archivedPath? }.
Ranch hub (ranch/api/.../bridle.controller.ts): real implementation. Reads data/sessions/bridle:<channel>.jsonl, writes the same content to data/sessions/bridle:<channel>.<iso-ts>.archived.jsonl, deletes the live file. No-op when the live file is empty/missing. Admins still have full history under .archived. files; visitor gets a clean slate.
Install
CDN — pinned:
<script src="https://bridle.cleanslice.org/sdk/v0.13.3.js" ...></script>NPM:
bun add @cleanslice/bridle@0.13.3Changelog: sdk-v0.13.2…sdk-v0.13.3
SDK v0.13.2 — New chat suppresses transcript replay
Patch on top of v0.13.1.
What v0.13.1 missed
v0.13.1 fixed the stale-socket race. But on token-flow embeds (the common production case) the clientId is fixed by the JWT sub — so the new socket's welcome arrives with the same clientId, the gen-guard correctly lets it through (it really is a fresh socket), loadTranscript() fetches /api/agent/<id>/transcript?channel=<clientId>, and the runtime's persisted history loads straight back into the freshly cleared list.
Symptom: New chat appears to clear the chat, then a second later the entire previous conversation re-appears.
Fix
One-shot skipNextTranscript flag. startNewChat() sets it right before the reconnect; the next welcome handler consumes it and skips its loadTranscript() call exactly once. Reload and ordinary reconnects keep their usual continuity (so refreshing the page still restores history — only New chat breaks it).
Same fix covers public-flow embeds once the hub starts honoring auth.anonId — both surfaces have the same "stable clientId by default" property.
Note: this is a client-side fix. The server-side transcript still exists; if the visitor opens the page on another device with the same JWT, the old conversation is still there. Purging it for real needs a hub endpoint we don't have yet — happy to add if needed.
Install
CDN — pinned:
<script src="https://bridle.cleanslice.org/sdk/v0.13.2.js" ...></script>NPM:
bun add @cleanslice/bridle@0.13.2Changelog: sdk-v0.13.1…sdk-v0.13.2
SDK v0.13.1 — New chat race fix
Patch fix for v0.13.0's New chat action.
Bug
After clicking New chat, the freshly cleared transcript reappeared a second or two later.
Root cause
Stale socket race. connect() calls client.disconnect() and immediately builds a new client, but the old socket can still deliver its welcome event for a tick or two — and the welcome handler from the previous connect() call hooked loadTranscript(oldClientId) to fire on that event. The HTTP fetch returns the old transcript and merges it into the freshly cleared message list.
Fix
Connection generation counter. Every connect() bumps connectGen; every callback captures the local value in its closure and bails if it no longer matches. loadTranscript() checks the gen both before and after its fetch so an in-flight request can't win against a subsequent New chat either.
Same guard hardens all the other rapid-reconnect paths — changing apiUrl / agentId / token / prompt props used to be similarly racy.
Install
CDN — pinned:
<script src="https://bridle.cleanslice.org/sdk/v0.13.1.js" ...></script>NPM:
bun add @cleanslice/bridle@0.13.1Changelog: sdk-v0.13.0…sdk-v0.13.1
SDK v0.13.0 — New chat action in a header menu
Highlight: header overflow menu with "New chat"
A 3-dot button now sits in the panel header, between the connection dot and the close button. Click it → small dropdown opens with New chat as the first action.
New chat does what you'd expect:
- Wipes the persisted
bridle:anon:<agentId>fromlocalStorage(the stable anon id introduced earlier). - Clears the local transcript.
- Reconnects — the hub mints a brand-new
clientIdon the next handshake. Old channel stays on the hub but this visitor never sees it again. Same effect as opening the page from a private window, no server-side teardown. - Resets the one-shot
greetingguard so the welcome message fires again on the fresh chat.
Why a header menu (and not a replaced paperclip)
Paperclip is the primary input action — hiding it behind a dropdown turns image upload into a 2-click flow. Header is the natural home for chat-level actions and scales for future items (Export, Mute, Clear, etc.) without rebalancing the input row.
Implementation notes
- Outside-click closes the dropdown via
composedPath()— needed to see through the shadow DOM. A document listener that comparedevent.targetto the menu would never fire (every target is the host<bridle-chat>element). - Escape closes the dropdown.
- Themable through
customCss— new internal classes:.bridle__menu,.bridle__menu-trigger,.bridle__menu-dropdown,.bridle__menu-item. - No new
init()options — the menu is always present in both floating and inline modes.
Install
CDN — pinned:
<script src="https://bridle.cleanslice.org/sdk/v0.13.0.js" ...></script>NPM:
bun add @cleanslice/bridle@0.13.0Full changelog: sdk-v0.12.2…sdk-v0.13.0
SDK v0.12.1 — Single checkbox layout fix
Patch for the standalone { type: "checkbox" } form component (introduced in v0.12.0).
The label was rendering below the checkbox and centered, instead of next to it. Caused by both bridle__ui-field (flex column) and bridle__ui-choice (align-items center) being on the same wrapper — column won and the center alignment re-purposed itself as horizontal centering on the cross axis.
Dropped the bridle__ui-field class on the single-checkbox case. Standalone checkboxes now render inline (checkbox left, label right) just like rows inside radio and checkbox-group fieldsets. Compound forms unaffected.
Install
CDN — pinned:
<script src="https://bridle.cleanslice.org/sdk/v0.12.1.js" ...></script>NPM:
bun add @cleanslice/bridle@0.12.1Changelog: sdk-v0.12.0…sdk-v0.12.1
SDK v0.12.0 — Interactive UI forms
Highlight: agents can render forms inside chat bubbles
Two new part types, one new handshake field, no protocol break.
ui(agent → browser) — render a form inside the assistant bubble:radio,checkbox,checkbox-group,select,input,textarea, plus layout helpersheadingandtext.ui_submit(browser → agent) — sent back as a regular user message when the visitor clicks Submit, carries{ uiId, values: {...} }.auth.capabilitieson the WebSocket handshake — SDK now advertises['streaming', 'images', 'files', 'ui']. The hub forwards this on everymessagepayload asmsg.capabilitiesso agent runtimes know whether the peer can render forms (Bridle yes, Telegram no).
import { buildUiForm } from '@cleanslice/bridle/runtime'
bridle.onMessage(async (msg) => {
if (!msg.capabilities?.includes('ui')) {
await bridle.send(msg.from, 'Reply with: basic, pro, or team')
return
}
const form = buildUiForm([
{ type: 'heading', text: 'Pick a plan' },
{
type: 'radio',
name: 'plan',
label: 'Plan',
required: true,
default: 'basic',
options: [
{ value: 'basic', label: 'Basic — \$0 / mo' },
{ value: 'pro', label: 'Pro — \$10 / mo' },
{ value: 'team', label: 'Team — \$30 / mo' },
],
},
{ type: 'checkbox', name: 'newsletter', label: 'Send me weekly updates' },
], { uiId: 'plan-2026-05', submitLabel: 'Continue' })
await bridle.send(msg.from, 'Quick one before we get you set up:', [
{ type: 'text', text: 'Quick one before we get you set up:' },
form,
])
})Next onMessage for that client carries the answers:
for (const part of msg.parts) {
if (part.type !== 'ui_submit' || part.uiId !== 'plan-2026-05') continue
const { plan, newsletter } = part.values
// provision, reply, send next form, whatever
}Behavior
- One-shot submit. Form disables itself after the first Submit; no double fire.
- Required validation. Inline error above the button on missing fields. Type coercion: checkbox → boolean, multi-checkbox → string[].
- uiId is required and agent-chosen — that's the only thing matching answers to questions across multi-step flows.
- User-side summary in the transcript: `Plan: pro · Send me weekly product updates: yes`. Transcripts stay readable without JSON dumps.
- Themable. `.bridle__ui`, `.bridle__ui-submit`, `.bridle__ui-choice`, etc. — full list in Theming.
Capability discovery
msg.capabilities is opt-in and may be undefined for pre-v0.12 SDKs or non-Bridle channels. Default to text when missing.
Docs
- Protocol → Interactive UI — wire format and behavior.
- Examples → 07 · Interactive forms — agent-side cookbook with capability-guarded fallback.
Hub & runtime changes
- Hub
BridleClientrecord now storescapabilitiesand forwards them on everymessagepayload to the agent. - Runtime adds
buildUiForm(...)helper with auto-uiIdso agents don't hand-roll IDs. IBridleMessageData.capabilitiesis the new field on the runtime side.
Backwards compatible: legacy clients without capabilities still work; agents not checking msg.capabilities won't crash — they just shouldn't emit ui parts blindly.
Install
CDN — pinned:
```html
```
NPM:
```bash
bun add @cleanslice/bridle@0.12.0
```
Full changelog: sdk-v0.11.0…sdk-v0.12.0
SDK v0.11.0 — Empty state with suggestion chips
Highlight: a real onboarding panel for empty chats
The default empty chat used to be a lone grey "Start a conversation". Now it can carry the agent's avatar, a headline, a sub-line, and a row of one-click suggestion chips that fire as user messages.
<script
src=\"https://bridle.cleanslice.org/sdk/latest.js\"
data-agent-id=\"agent-abc-123\"
data-token=\"<jwt>\"
data-empty-avatar=\"/avatars/support-agent.png\"
data-empty-title=\"How can I help?\"
data-empty-subtitle=\"I can compare plans, schedule a demo, or open a ticket.\"
data-suggestions=\"Compare plans|Book a demo|Open a support ticket\"
></script>Live demo and a combine-with-greeting recipe on 06 · Empty state.
Four independent options
All four are optional. Provide none → the legacy single-line copy stays. Provide any → the rich panel renders.
| Option | init() |
data-* |
|---|---|---|
| Agent avatar (URL) | emptyAvatar |
data-empty-avatar |
| Headline | emptyTitle |
data-empty-title |
| Sub-line | emptySubtitle |
data-empty-subtitle |
| Suggestion chips | suggestions: string[] |
data-suggestions=\"Q1\|Q2\|Q3\" |
Programmatic accepts string[]. Script-tag accepts either a JSON array string or a |-separated string — easier to write in HTML without escaping.
Behavior
- Hidden as soon as
messages.length > 0or the agent starts replying. Chips don't flash next to a streaming reply. - Clicking a chip is equivalent to typing the same text and pressing Send —
onMessagefires for the user turn, the runtime sees it as any other message. - Chips are disabled while the WebSocket isn't open.
Stacks naturally with greeting + prompt
| Feature | Audience |
|---|---|
Empty state (emptyAvatar, emptyTitle, emptySubtitle, suggestions) |
What the visitor sees in an empty chat. |
greeting / greetingDelay (v0.10.0) |
What the agent "says" once typing dots clear. |
prompt |
What the agent runtime receives at handshake. Hidden from visitor. |
Install
CDN — pinned:
```html
```
NPM:
```bash
bun add @cleanslice/bridle@0.11.0
```
Full changelog: sdk-v0.10.0…sdk-v0.11.0
SDK v0.10.0 — Welcome message
Highlight: pre-seeded first assistant bubble
Two new init options — greeting and greetingDelay (also data-greeting / data-greeting-delay) — let you pre-seed the first message a visitor sees when they open an empty chat.
<script
src=\"https://bridle.cleanslice.org/sdk/latest.js\"
data-agent-id=\"agent-abc-123\"
data-token=\"<jwt>\"
data-greeting=\"Hi! 👋 I can help you compare plans, schedule a demo, or open a support ticket. What are you here for?\"
data-greeting-delay=\"3000\"
></script>Typing dots appear for greetingDelay ms (default 3000), then the message fades in as a regular assistant bubble. Markdown is rendered.
Suppression rules — won't shove onto real content
- Watcher fires only when panel open + connected + transcript empty, after the transcript-replay race has settled. Returning visitors with history never see it.
- After the delay we re-check
messages.length— if the user typed and sent something during the wait, their turn wins and the greeting is dropped. - One-shot per session. Close + reopen does not re-trigger. Reconnect (apiUrl/agentId/token/prompt change) resets the flag with the transcript.
Programmatic
init({
apiUrl,
agentId,
token,
greeting: 'Hi! 👋 What brings you here?',
greetingDelay: 3000, // ms; 0 to skip the delay
})Common patterns
- Onboarding hint — tell the visitor what the bot can actually do so they don't have to ask first.
- Page-tuned greeting — combine with
data-promptfor section-aware copy (pricing vs. support vs. docs). - Off-hours notice — "I'm a bot. For live support after 8pm, leave your email and we'll reply in the morning."
Full docs: Welcome message.
Install
CDN — pinned:
```html
```
NPM:
```bash
bun add @cleanslice/bridle@0.10.0
```
Full changelog: sdk-v0.9.0…sdk-v0.10.0
SDK v0.9.0 — Image attachments
Highlight: image attachments in the chat input
The widget now accepts images the same way every modern messenger does:
- Click the paperclip in the input row.
- Drag a file onto the chat panel — full-panel drop overlay highlights the target.
- Paste from the clipboard while the textarea is focused (screenshot-friendly).
Attachments stage as 56×56 thumbnails above the textarea and only ship when you press Send — text and images go out together as one message. The agent receives them as a parts[] array (text + images), so the caption is right next to its picture.
// What the runtime sees on the wire — already supported, no protocol change:
{
text: 'what's wrong with this layout?',
parts: [
{ type: 'image', base64: '<...>', mediaType: 'image/jpeg' },
{ type: 'text', text: 'what's wrong with this layout?' },
],
}Smart payload sizing
Files over 800 KB are drawn into a canvas, capped at 1280 px on the longest edge, and re-encoded as JPEG @ 0.85 — the Socket.IO payload stays under the default 1 MB buffer without any hub config change. Smaller files keep their original encoding so PNG transparency survives.
Limits
- Max 5 attachments per message.
- Max 10 MB per file (raw).
- Non-image files are silently rejected with a console warning.
Assistant images now render too
Both user and assistant bubbles draw image parts inline. The runtime could already send back images via the protocol — the SDK simply wasn't drawing them.
New themable classes
Added to Theming → Internal classes:
| Class | Purpose |
|---|---|
.bridle__attach |
The paperclip button |
.bridle__attachments |
Thumbnail strip above the input |
.bridle__attachment / .bridle__attachment-img / .bridle__attachment-remove |
A staged thumbnail and its × button |
.bridle__drop-overlay / .bridle__drop-hint |
Drag-over visual |
.bridle__msg-image |
Image rendered inside a message bubble |
All targetable via `customCss` / `data-custom-css`.
Install
CDN — pinned:
```html
```
NPM:
```bash
bun add @cleanslice/bridle@0.9.0
```
Full changelog: sdk-v0.8.2…sdk-v0.9.0
SDK v0.8.2
First GitHub release since the project moved past sdk-v0.6.1. Bundles everything shipped in 0.7.x → 0.8.2; CDN and npm have been live throughout.
Highlights
fabIcon — brand the floating button (v0.8.2)
New init option (also wired as data-fab-icon) replaces the built-in chat-bubble glyph on the floating FAB with any image URL — .svg, .png, .webp, or a data: URI. Falls back to the default glyph when unset.
Bridle.init({ apiUrl, agentId, token, fabIcon: '/icons/chat.svg' })Docs: Theming → Custom FAB icon.
Full-bleed chat panel on narrow viewports (v0.8.1)
At ≤480px the floating panel now pins to all four edges with room for the FAB, instead of the cramped 380×560 default — much better with the on-screen keyboard up.
customCss / data-custom-css (v0.8.0)
Inject CSS straight into the shadow root for internal classes (.bridle__panel, .bridle__bubble, …) that host-page CSS can't reach. Pairs with data-stylesheet for external files.
Transcript replay on welcome (v0.7.0)
The hub now replays the recent transcript on connect, so reloading the page keeps the conversation in view instead of starting fresh.
Docs site
- New Examples section, promoted to first in the top nav: Basic · Inline · Styles · Authenticator.
- Every Examples page renders a live
<BridleEmbed />against the same public demo agent as the homepage hero. - Authenticator example ships a code-group with Node / Python / PHP / Java / C# implementations of the
/embed/tokenendpoint.
Install
CDN — pinned:
```html
```
CDN — track the 0.x line:
```html
```
NPM:
```bash
npm install @cleanslice/bridle@0.8.2
```
The IIFE bundle (bridle.js) and ESM bundle (bridle.mjs) are attached below for direct download.
Full changelog: sdk-v0.6.1…sdk-v0.8.2