Drop-in chat UI
read as.md@guuey/chat (MIT) is the default chat UI for guuey agents — the same transcript tier guuey’s own surfaces run: the embeddable widget, Studio’s test chat, and the Portal apps all render through this package. Mount it with your agent’s endpoint and you get a production chat surface; keep customizing until only the parts you care about are yours.
The design bar it holds: an agent transcript stays readable in every weird case — a 40-second turn running a dozen tools, a generative view mounting mid-stream, a failed tool, a cold-started agent, a dropped connection. Each of those is a designed state, not an accident.
npm install @guuey/chatTen lines
Section titled “Ten lines”"use client";
import { GuueyChat } from "@guuey/chat/react";import "@guuey/chat/styles.css";
export function AgentChat() { return ( <GuueyChat endpointUrl="https://your-agent-endpoint" // printed by `guuey deploy` appId="your-app-id" /> );}Both values come from your deploy: the endpoint is printed by guuey deploy (and shown by guuey apps get), the app id by guuey apps list. Before your page goes to production, allow its domain and verify with guuey apps check --origin — same rules as any custom surface.
What you get for free
Section titled “What you get for free”- Streaming text as sanitized markdown — raw HTML from the model is structurally unrepresentable in the rendering pipeline, not merely escaped.
- Tool activity as calm one-liners — a running tool is a live status line; consecutive finished tools collapse into one expandable “Ran N tools” row; a tool that produces something visible (a generative-UI card, media, a prompt) always renders in place, never buried in a collapse.
- Generative-UI cards mount sandboxed — agents using ggui render interactive views inline, in a locked-down iframe, with the negotiation states labeled (“no host answer” instead of a blank frame).
- Cold starts are narrated — “Connecting…” escalates to “Starting your agent…” with a patience note, driven by the platform’s real turn lifecycle.
- Dropped connections recover themselves — if a stream goes silent while the agent actually finished, the UI adopts the completed reply from persisted history instead of freezing on a cursor.
- Errors arrive as human copy — coded platform errors (quota, auth, transient) render family-appropriate wording with an action slot, never a raw code string.
- A composer that knows the turn state — Enter sends, Shift+Enter newlines, IME composition is respected, Send becomes Stop mid-turn, and stopping keeps the partial reply marked “Stopped.”
- Accessibility as a default — streaming text and status announce via live regions, every collapsible is keyboard-operable, prompts manage focus, animations respect reduced-motion.
- Scroll behavior that holds — pinned to the latest message while streaming, released the moment the reader scrolls up, with a jump-to-latest affordance; long transcripts render windowed.
Everything above is the calm preset — the end-user default. Unknown or future content types render as labeled, collapsed rows: never blank, never raw JSON.
Give visitors a stable identity
Section titled “Give visitors a stable identity”The default setup carries identity via cookies, which is best-effort for cross-origin embeds. For durable anonymous continuity, mint a guest secret and hand it to the component — the same identity model as the underlying SDK:
"use client";
import { GuueyChat } from "@guuey/chat/react";import "@guuey/chat/styles.css";
/** 64 lowercase hex chars, minted once and persisted. Returns null if storage is blocked. */function getGuestSecret(): string | null { try { const key = "guuey:guest-secret:your-app-id"; const existing = window.localStorage.getItem(key); if (existing && /^[0-9a-f]{64}$/.test(existing)) return existing; const bytes = crypto.getRandomValues(new Uint8Array(32)); const secret = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); window.localStorage.setItem(key, secret); return secret; } catch { return null; }}
export function AgentChat() { return ( <GuueyChat endpointUrl="https://your-agent-endpoint" appId="your-app-id" apiBaseUrl="https://api.us-east-1.guuey.com/v1" getGuestSecret={getGuestSecret} /> );}apiBaseUrl is what turns on the batteries-included read paths: transcript history on reload, and resolution of generative-UI views by read (the component builds a reader over the same identity for you). Signed-in users work the same way as the SDK’s bring-your-own-auth: pass getAccessToken instead of getGuestSecret. If you construct your own adapters or reader, they always win over these defaults.
First customization
Section titled “First customization”Three dials cover most products, all as props:
<GuueyChat endpointUrl="https://your-agent-endpoint" appId="your-app-id" preset="debug" // expanded tools + wire detail — for internal tools and admin views strings={{ composerPlaceholder: "Ask about your booking…" }} policy={{ toolGroup: { threshold: 4 } }}/>preset—"calm"(default, end-user) or"debug"(everything expanded, wire codes visible — what Studio’s test chat uses).strings— every piece of UI copy in one override map, which is also the i18n seam.policy— per-category behavior knobs (grouping thresholds, collapse defaults, error wording, result size caps).
When props stop being enough, the same package exports every layer underneath — replace one component, keep the rest; or take the headless view-model and render everything yourself. That ladder is the next page: Customization & presets.
Suggested prompts
Section titled “Suggested prompts”Chips (or any host-driven send) stay on the batteries-included path through the imperative handle — a ref (or the onReady callback, same object):
import { useRef } from "react";import { GuueyChat, type GuueyChatHandle } from "@guuey/chat/react";
function Assistant() { const chat = useRef<GuueyChatHandle>(null); return ( <> <div className="chips"> {["What can you do?", "Plan my week"].map((p) => ( <button key={p} onClick={() => chat.current?.send(p)}> {p} </button> ))} </div> <GuueyChat ref={chat} endpointUrl="https://your-agent-endpoint" appId="your-app-id" /> </> );}send runs through exactly the Send button’s gate — it returns false (and does nothing) while a turn is in flight, when chat is unavailable, or for blank text — and it never touches a half-typed draft. prefill(text, { append }) and focusComposer() ride the same handle for stage-then-edit flows, and handle.threadId (or the onThread prop) tells you which thread you’re on without wrapping storage. Details: Customization & presets.
Theming
Section titled “Theming”<GuueyChat theme={…} mode="dark"> takes a serializable theme object — see Theming. The default theme is brand-neutral; the guuey look ships as a second exported constant.
Strict Content-Security-Policy hosts
Section titled “Strict Content-Security-Policy hosts”If your page ships a strict CSP, two things are worth knowing before the first generated view renders.
Views need origins your policy may not list. A generative-UI view boots a runtime bundle and opens a live channel to its MCP host — under a script-src/connect-src that omits those origins, the frame stays blank and the kit can only say “this view didn’t start”. Two aids:
- Pass the view’s declared origins so the host can name the cause:
<GuueyChat viewProps={{ cspOrigins: { resourceDomains: ["https://assets.<mcp-host>"], connectDomains: ["https://<mcp-host>", "wss://<mcp-host>"] } }}>. When your page loads or connects to one of those origins and your policy refuses it, theno-handshakelabel upgrades to the exact allowance — “addscript-src-elem https://assets.<mcp-host>” — and, underpreset="debug", the same verdict reachesonDebugEventasview-phase.diagnosis. (The upgraded label shows under the default calm preset too; the sink only fires under the debug preset.) - Know the tripwire’s reach: it listens on your document, so it sees violations your page incurs on the view’s origins (a runtime bundle you load at page level, a channel you open on the view’s behalf). A
srcdocview’s own blocked loads report inside the frame’s opaque origin, where nothing outside can listen — for those, the CSP console message names the blocked URL, and the fix is the same allowance. (Views mounted through asandboxPageUrlrelay carry their own policy and are unaffected by yours.)
One script-src eval report is not the view. @guuey/chat reaches zod v4, which probes new Function("") once at boot to decide whether to compile fast parsers — under a policy without 'unsafe-eval' that probe is caught, harmless, and still reported as a securitypolicyviolation (zod’s own source says so). It is a false positive on exactly the channel the tripwire watches. Silence it by opting zod out of the probe before its first schema parses — as a side-effect module that is your entry’s first import:
// csp-jitless.ts — import this FIRST in your entryimport { z } from "zod";z.config({ jitless: true });import "./csp-jitless";import { GuueyChat } from "@guuey/chat/react";A later call runs after the probe and changes nothing.
Widget or kit?
Section titled “Widget or kit?”The two-script widget embed is the no-code path: guuey’s origin, guuey’s updates, zero build. @guuey/chat is for when the chat should be inside your product — your bundle, your router, your design system. Both render the same transcript tier; you are not trading capability for control.