Skip to content

Chat bubble

In review
Live · web

Roles: agent leads (left), user trails (right), system centers

Routing every session through the credential-store proxy for credentials.

Perfect. Ship it.

Grouping: a same-sender run keeps the tail only on the edge bubbles

Reading the reconnect loop.

Backoff caps at 30s with jitter.

Re-running the SSE tests now.

Nice.

Add a metric for reconnect count too.

Delivery states: own-message lane (status is warm + icon + text, never color alone)

Cap the retry count at 3.

Sending10:26 AM

Use exponential backoff.

Sent10:25 AM
FailedCouldn’t send: no connection to the gateway.
Not delivered

Selected + timestamp: tap a bubble to select (a cyan ring, no wash, not a glow)

10:21 AM
10:22 AM

Retry fired 0 times · selected: sel · tab to a bubble or Retry; the island is live.

// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform;
// this is the intended @dizyx/nockerl-react API (not yet exported; promotion tracked),
// modeled on the shipped Compose bubble container.) The bubble is the PRIMITIVE
// that holds one message; AgentMessage composes its header + body ON it.
export function Thread() {
return (
<>
{/* agent → leading/left soft plane; user → trailing/right cyan jewel */}
<ChatBubble role="agent">Routing through the credential store.</ChatBubble>
<ChatBubble role="user">Perfect, ship it.</ChatBubble>
{/* a same-sender run: tail only on the edge bubbles, squared seams between */}
<ChatBubble role="agent" group="first">Reading the reconnect loop.</ChatBubble>
<ChatBubble role="agent" group="last">Re-running the SSE tests.</ChatBubble>
{/* own-message delivery lane + a centered system bubble */}
<ChatBubble role="user" delivery="sent" timestamp="10:25 AM">Use backoff.</ChatBubble>
<ChatBubble role="user" delivery="failed" onRetry={resend}>Couldn’t send.</ChatBubble>
<ChatBubble role="system">Context compacted</ChatBubble>
</>
);
}
PropTypeDefaultDescription
role *'user' | 'agent' | 'system'Speaker. user → trailing/right (cyan jewel card); agent → leading/left (soft lifted plane); system → centered, muted divider bubble.
childrenReactNodeThe message body. The bubble is a pure container. It does not render a header, markdown, or tool panels (that is AgentMessage).
group'single' | 'first' | 'middle' | 'last''single'Position in a consecutive same-sender run. The tail stays on the edge bubbles (first/last); interior speaker-side corners square off and spacing tightens.
delivery'sending' | 'sent' | 'failed'Own-message delivery state. Adds a warm receipt (clock · check · alert) under the bubble; failed restyles the bubble to the banner inline-alert grammar (inset status disc + whisper-red border + lifted neutral surface) with title, message and Retry. Never color-alone.
timestampstringShort time string shown in the meta line under the bubble (e.g. "10:23 AM").
selectedbooleanfalseSelected / long-pressed: a cyan ring at the selection weight, with no wash (law §6, state reads by outline) and never a glow.
onSelect() => voidTap-to-select handler for selection (a toggle, marked by the cyan ring), distinct from the long-press pop menu (a menu of actions); the two compose. When set, the whole bubble becomes one focusable button (one accessible name).
onRetry() => voidRetry handler for a failed bubble; renders a real, focusable Retry button.
Cross-platform drift (tracked for reconciliation)
  • Primitive vs. composition: this is the bubble container (shape · tail · role alignment · grouping · delivery/selection). The full assistant turn (identity header, markdown, tool-call panels, streaming) is Agent message, composed on this bubble. Tool panels and markdown are documented there, not here; note the bottom-of-chat streaming indicator there is design-only (documented, not shipped in the products).
  • Swift: no chat surface ships yet; Voice is transcription-only. The Swift API above is intended, not implemented.
  • Bubble radius: Compose MessageBubble hardcodes the shape at 18.dp / 6.dp, while bubbleShape (and the web demo) default to the token 20 / 6 (radius.bubble / radius.bubble-tail). The bubble call-site should consume the token.
  • Grouping seams (resolved): the web grouping model wins: interior speaker-side corners of a same-sender run square off, one tail per run. Compose (today collapses only the header, keeping every bubble’s full tail) conforms.
  • Delivery receipts (resolved): the sending / sent / failed lane + the in-thread failed bubble built on the Banner inline-alert grammar (inset status disc + whisper-red border + lifted neutral surface + Retry) is the ratified model. Android moves stream errors off the floating status pill and per-bubble.