v0.2.0
Changed
-
Widget: the composer grows with the message. The message field was an
<input>, which can hold exactly one line no matter how long the message is β anyone reporting a real problem typed into a slot that showed them ~40 characters of what they had written. It is now a<textarea class="in">that starts at one line and animates its height as the content wraps, up to 5 rows, after which it scrolls (.in.tall). Height is animated (0.18s), and dropped underprefers-reduced-motion. Enter still sends andShift/Ctrl/Cmd+Enteropens a new line;isComposingis honoured so Enter never sends mid-IME-composition in Japanese/Chinese/Korean input. The composer isalign-items:flex-endso the send button stays on the last line rather than drifting to the middle of a paragraph. Sending resets the field to one line on the same transition.Themers: the selectors
\.ft input,\.ft input:focusand\.ft input::placeholderare now\.ft .in,\.ft .in:focusand\.ft .in::placeholder..cap input(the lead form) is unchanged. Anything overriding the composer by element name needs the class instead.
Added
- Release process β the repo now cuts semver tags + GitHub Releases via
node scripts/release.mjs <x.y.z>(promotes[Unreleased]β a dated version section, bumps thepackage.jsonversions, tagsvX.Y.Z, publishes the GitHub Release from the changelog section).deploy.sh productionrefuses to ship a commit that isn't av*tag (break-glass:ALLOW_UNTAGGED_DEPLOY=1), so every production deploy is a real, changelog'd release. SeeRELEASING.md. - Widget:
theme.launcherStyle: "pill"β an opt-in labelled launcher, and the animated states that go with it. The pill carries the avatar, a hairline separator and a word (theme.launcherLabel, β€24 chars, defaults to the widget title). While the panel is open it collapses to the avatar alone and sheds its frame, so the launcher and the panel stop competing for the same corner; closing builds it back on the same transition. The leading edge lights up inprimaryColorfor the duration of the move (.kmoving) β it is pinned to the pill's own right edge, so the width transition carries it with no second animation to keep in sync. Unread lights the separator itself and breathes it (kattend, 3.2s) rather than adding a badge. Everything is gated on.kpill: with no theme, or any value other than the literal"pill", the launcher is byte-identical to today (asserted by a control run). Honoursprefers-reduced-motionβ the states survive, the motion does not.
Fixed
- Widget: the notification ding was unreachable for anyone whose visitors interact with the page before the widget. Audio is gated on user interaction and the unlock was armed from a single listener on the widget's own host, so a visitor who had been using the page for ten minutes but had not yet touched the launcher got silence β and an operator running a custom launcher outside that host got silence permanently. The first
pointerdownanywhere in the document now arms it (once, capture), in addition to the existing host listener. - Widget: unread no longer forgets itself on navigation.
kunreadwas a class with a one-page lifetime, so an inbound message that arrived while the visitor was reading something else vanished the moment they refreshed or moved on. It is now remembered underkrispy_unread_<tenant>, restored on boot when the panel is closed, and cleared by opening and nothing else β the same contract the class already had, made durable.localStoragefailures are swallowed (Safari private mode, blocked cookies): the fallback is exactly today's behaviour.
Added
- Edge: kbase injection β
TenantConfig.kbSources(KbSource[]:{ id, name, text, updatedAt }, total text still hard-capped at 100K chars) is now assembled into the system prompt at chat time as a## Knowledgeblock (runtime assembly, not compile-on-save), between the persona and the forms/guardrail contracts. Unset β the prompt is byte-for-byte what it is today.detectPromptLeakis narrowed to the instruction portion (the same prompt without the knowledge block) so a bot quoting its own KB verbatim is no longer false-flagged as a prompt leak and forced into a handoff. - Edge: relearning from handoffs β off by default: it runs only for a tenant already using the knowledge base (β₯1
kbSource), so a tenant that never configured a KB incurs zero billable AI on handback and behaves exactly as today. When a handed-off session hands back to the AI (operator resolves it, or the silence alarm fires) and it had β₯1 operator message, theSessionDOmakes one Workers-AI call (metered underai) to extract at most one QβA the human answered that the bot couldn't, and appends it as aKbSuggestion({ id, question, answer, createdAt }) under its own per-site KV keysuggestions:<ns(t,s)>(cap 20 FIFO; normalized-question dedup against pending suggestions and approvedkbSources). A separate key means the background write can never race a human config save. Failures are swallowed β relearning never breaks resolve. The DO now persists itstenantId+siteIdwrite-once on thePOST /contextread (the handback fires from an alarm with no request in flight and the DO can't derive them from its own name).TenantConfig.kbVersionbumps on anykbSourceswrite. - Edge: approval inbox routes (secret-authed,
?s=/siteIdscoped like the other config routes) βGET /api/tenant/kb-suggestions?t=&s=(list pending),POST /api/tenant/kb-approve { tenantId, siteId?, id }(move intokbSources+ bumpkbVersion;404on unknown id),POST /api/tenant/kb-dismiss { tenantId, siteId?, id }(drop it). Suggestions are never projected to the public widget config. Human-in-the-loop by design: the bot never self-modifies its KB. - CLI: self-host parity for the approval inbox β
krispy kb-suggestions(list pending),krispy kb-approve <id>(append tokbSources+ bumpkbVersion),krispy kb-dismiss <id>(drop it), over the secret-authed edge routes above. Each takes an optional--site <id>flag (β?s=/siteId) to scope a single site. - Multi-site (edge + widget foundation) β one account can run multiple sites, each with its own config blob (theme, persona, connectors, forms, popups, kbase) and liveness. A site is an optional
?s=<site>(query) /data-site(embed) /siteId(chat & lead body) that suffixes the tenant's KV namespace vians(t, s). An absent ordefaultsite collapses to the exact legacy key β every existing tenant is untouched and nothing migrates (asserted by test).siteIdis charset-guarded (/^[a-z0-9_-]{1,40}$/) at the trust boundary before it enters a:-delimited key; malformed β400. Config-consuming routes (/api/widget/config,/api/chat,/api/lead,/api/tenant/configGET+POST,/api/tenant/liveness) resolve it; conversations (session/thread/DO) and billing (usage/entitlement) stay keyed by tenantId alone β pooled quota per account, per-site conversations deferred. The cloud dashboard site-switcher + site table land next.
Added
- Widget liveness β the widget's boot-time
GET /api/widget/confignow doubles as a heartbeat: the edge stamps a per-tenant last-seen record (timestamp + the embedding page's origin/url fromOrigin/Referer), throttled in-isolate to stay well under KV's write budget and never blocking the boot. New secret-authedGET /api/tenant/liveness?t=<tenant>returns{ seen }so the dashboard can show "live on example.com β last seen 2m ago". Stores last-seen only for now (per-origin set + per-site keying are the multi-site upgrade path).
Added
- Edge: new
WidgetThemeknobs βglowColor,tagline,sparkle,direction,popupText, andtiming(WidgetTiming:launcherDelayMs/sparkleAfterMs/popupDelayMs/popupCooldownHrs/autoOpenMs) β projected through the publicGET /api/widget/configwhitelist. All default unset/off: a tenant that configures nothing gets today's neutral widget unchanged. - Edge: hard write caps on
POST /api/tenant/config(trust boundary; invalid configs never reach KV) βtheme.avatarβ€48KB + scheme check (buttr|https://|data:image/(png|webp|jpeg);base64,), connector CTA urls https-only, free-texttheme.tagline/theme.popupTextβ€500 chars,kbSourcestotal text β€100K chars. Size overruns β413; malformed values β400. - Edge:
TenantConfiggainspersona(PersonaSpec:toneOfVoice+styleRules[]),script(ConversationScript:opening[]+starters[]), andpopups(PopupSpec[]: timer/section-proximity teaser engine;theme.popupTextis sugar for one timer popup). All default unset β the bot speaks/opens exactly as today until configured. - Edge: the
Connectorunion grows CTA fields (ctaopt-out,label,caption,showAfterMs) and new types (phone,facebook,tiktok,link, each with a server-builturl/tel:/wa.mehref).cta: falsekeeps an otherwise-CTA connector delivery-only. - Edge:
GET /api/widget/configprojection now also servesctas(CTA-capable connectors minuscta:falseopt-outs, with server-built hrefs + default labels),script(opening β€5 / starters β€4), andpopups(or thepopupTextsugar).personais structurally excluded β it is instruction text folded into the system prompt only. - Edge:
buildSystemPromptfoldspersonainto the prompt (## Voice+## Style rules) between the tenant instructions and the guardrails, so it sits inside thedetectPromptLeakscope (a bot reciting its own tone verbatim counts as a leak). - Edge: additional
POST /api/tenant/configwrite caps βpopupsβ€8 entries, popuptextβ€500 chars, popup selectors (nearselector /cancelOnClick) β€200 chars,script.openingβ€5,script.startersβ€4, andpersona+scriptcombined free text β€8K chars (all413). - Edge:
GET /api/widget/confignow sendsCache-Control: public, max-age=60β the boot config grew to ~10β30KB with data-URI avatars and was refetched uncached on every page load; 60s keeps edits near-live. - Widget: the new theme knobs come alive β
primaryColornow drives the visitor bubble, send button, and input focus ring (was a dead CSS var);launcherColorfills a badge circle behind the launcher mascot;glowColoradds an opt-in launcher glow (no glow layer at all when unset);sparkleadds a 10s idle shadow-swell + conic-ring loop aftersparkleAfterMs;taglinereplaces the header sub-line;direction: "rtl"flips the panel (bubble corners, input dir, mirrored send icon);timing.launcherDelayMs > 0hides the launcher then plays a one-time entrance pop (skipped on revisit viasessionStorage);timing.autoOpenMs(opt-in; default 0 = never) auto-opens a closed panel after an inbound reply. All animations respectprefers-reduced-motion. - Widget: proactive timer popup β
theme.popupTextshows a dismissible teaser card above the launcher aftertiming.popupDelayMs(default 8s) with a per-tenanttiming.popupCooldownHrscooldown (default 24h) inlocalStorage; suppressed while the panel is open; clicking it opens the chat. Unset = nothing ever shows. - Widget: avatars accept
data:image/β¦URIs in addition to"buttr"and https URLs (sharedisRenderableAvatar()gate). - Edge/widget:
FormSpec.successTextβ the line the submitted lead-form card collapses to in the transcript (widget default: "Thanks β we'll be in touch."). - Edge: lead emails set Resend
reply_toto the lead's captured email (the form'semail-typed field) β the tenant hits Reply and talks to the lead; omitted when no email was captured. - Edge: Telegram quiet ops β routine mirrors post silently (
disable_notification), and a handoff@mentionsthe tenant's operators (viatext_mentionentities, no public username needed) so notifications fire only when a human is needed. NewTenantConfig.operators(auto-learned from topic replies, capped at 10, never exposed to the public widget config). - Tilt:
KRISPY-COREbanner resource in its own capitalized label group β names the dev dashboard (Tilt has no native project title). - Chat suite: lead capture + connectors β edge fan-out and a data-driven widget lead form.
- Chat suite: widget theming β
:host--k-*CSS vars, boot-fetch of tenant config, avatar/greeting/position, CSS-boundary sanitizers. - Chat suite: keyboard-aware floating widget card on mobile (visualViewport pin, safe-area, 16px inputs).
- Chat suite: message notifications β WebAudio ding, launcher pulse, unread dot, visitor mute (
theme.sound). - Chat suite: safe minimal markdown in bot/operator bubbles (bold/italic/code/links, XSS-safe; visitor text stays literal).
- Chat suite: security guardrails β always-appended
SECURITY_INSTRUCTIONin the system prompt (refuses prompt/architecture/secret disclosure, injection resistance); survives custom tenant prompt overrides. - Docs site: Fumadocs documentation site under
apps/docs(quickstart, concepts, security, guides, reference for edge routes / tenant config / CLI / markers). - Governance:
CHANGELOG.md+ documentation-sync rule (AGENTS.mdΒ§7). - CI: build gate β CI now builds
apps/docs(present-guarded) after typecheck/test. - API contract:
api-collection/openapi.yaml(OpenAPI 3.1) covering the edge Worker's full HTTP surface; new Bruno requests for/api/chat,/api/contact,/api/lead,/api/widget/config,/api/usage.AGENTS.mdΒ§7 now requires OpenAPI + Bruno on any route change. - Deploy: Tilt
deploy:*manual resources +./deploy.sh <edge|docs|widget> <preview|production>(preflight β build βwrangler deployβ smoke) withscripts/cf-deploy-preflight.mjs+scripts/cf-deploy-smoke.mjs; namedpreview/productionwrangler envs for the edge Worker. Cloudflare creds sourced from Infisical-fed.env.local, never GitHub Actions. - CLI:
krispy logo <file>β removes a logo's background locally (no cloud, no API key) via corner chroma-key and prints a paste-readydata:image/png;base64,β¦avatar URI fortheme.avatar(self-host parity for the cloud dashboard's AI bg-removal). Handles png/jpg/webp/svg, downscales to 144px; leaves already-transparent or busy-background images untouched. Depends onsharp(lazy-loaded so the CLI core stays dep-free at import). - Release:
@krispyai/cliis now publishable (0.1.0, public) via npm Trusted Publishing (.github/workflows/publish.yml, OIDC, no npm token); other packages stay private. Founder setup + first-publish bootstrap documented inAGENTS.mdΒ§Β§10β11. - Domains:
krispyai.comβ prod hostnamesedge.krispyai.com/docs.krispyai.com/widget.krispyai.comasdeploy.shsmoke defaults, OpenAPI prod server, and embed-snippet examples. Custom domains attached in the CF dashboard after the first deploy (NS transfer pending); no hostname is hardcoded in runtime source.
Changed
- Widget: handoff-without-form now renders a built-in default contact
FormSpecthrough the one lead-form renderer (posts/api/lead); the legacy static.capmarkup +showCapture()path is gone./api/contactstays as an edge shim for already-deployed widgets. - Edge hardening per security audit: self KV-config merge, fetch timeouts, Telegram mirror best-effort, WS backoff cap, lead rate-limit, DO internal auth.
Fixed
- Widget: a tenant avatar now reaches the floating launcher badge too β
applyThemeset only the header avatar'ssrc, so the launcher always kept the default mascot no matter what the tenant configured. - Widget: the lead form is no longer a sticky band pinned between the log and the composer β it renders inside the message log as a bubble-style card that scrolls with the conversation (
scrollIntoViewon insert) and collapses in place to a compact record after submit. - CI: gitleaks runs via the free CLI image (the GitHub action needs a paid org license β was red on every PR, blocking dependabot).
- README: real Buttr hero image (was a leftover builders-stack architecture diagram).
- Dev servers run on fixed ports β edge (wrangler) + widget; portless is an alias only.
- Docs deploy:
apps/docsnow fully static-exports (output: 'export') and./deploy.sh docsuploadsout/to CF Pages directly β replaces the broken@cloudflare/next-on-pagespath (which requiredruntime = 'edge'on/api/search+/docs/[[...slug]]and then failed to edge-bundlelib/source.ts). Search switched to the build-time Orama static index (staticGET+search.type: 'static'); smoke now also checks/api/searchfor the docs site.