Chat bubble
Roles: agent leads (left), user trails (right), system centers
Routing every session through the credential store 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.
Use exponential backoff.
Selected + timestamp: tap a bubble to select (cyan ring + faint wash, not a glow)
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> </> );}// Android: Jetpack Compose (canonical). The shipped bubble container is// com.nockerl.app.chat.ui.MessageBubble; its surface = NockerlCard + the// rememberBubbleShape tail. role drives alignment (USER = trailing/right cyan// gradient, ASSISTANT = leading/left cardAlt plane, SYSTEM = a centered divider).import com.nockerl.app.chat.ui.MessageBubbleimport com.nockerl.app.core.theme.BubbleTailimport com.nockerl.app.core.theme.rememberBubbleShape
// One bubble per message. The list collapses the header on a same-speaker run// (showHeader = !isContinuation) and caps the width at the phone bubble cap.MessageBubble( message = message, // role → alignment + surface; carries blocks showHeader = !isContinuation, // grouping: collapse on a consecutive same-sender turn accentColor = Color.Unspecified, // Unspecified → the default cyan userCard gradient bubbleMaxWidth = layout.bubbleMaxWidth, // 320.dp phone cap; wider on tablets)
// The bubble silhouette itself. The tail corner points at the speaker:val shape = rememberBubbleShape( tail = if (isUser) BubbleTail.TOP_RIGHT else BubbleTail.TOP_LEFT, radius = 20.dp, // radius.bubble tailRadius = 6.dp, // radius.bubble-tail)// macOS (SwiftUI). NOT YET IMPLEMENTED: Nockerl Voice is a dictation/transcription// surface and ships no chat thread, so there is no bubble primitive on macOS. This// is the INTENDED API, derived from the laws + the Voice material (NockerlTheme,// the lifted card) so the bubble matches Android when a chat surface lands.ChatBubble(role: .agent) { Text("Routing through the credential store.") }ChatBubble(role: .user) { Text("Perfect, ship it.") }
// a same-sender run: tail only on the edge bubblesChatBubble(role: .agent, group: .first) { Text("Reading the reconnect loop.") }ChatBubble(role: .agent, group: .last) { Text("Re-running the SSE tests.") }
// own-message delivery lane + a centered system bubbleChatBubble(role: .user, delivery: .sent, timestamp: "10:25 AM") { Text("Use backoff.") }ChatBubble(role: .system) { Text("Context compacted") }Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
role * | 'user' | 'agent' | 'system' | Speaker. user → trailing/right (cyan jewel card); agent → leading/left (soft lifted plane); system → centered, muted divider bubble. | |
children | ReactNode | The 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. | |
timestamp | string | Short time string shown in the meta line under the bubble (e.g. "10:23 AM"). | |
selected | boolean | false | Selected / long-pressed: a cyan focus ring + a faint cyan wash (the single brand accent), never a glow. |
onSelect | () => void | Tap-to-select handler for selection (a toggle + soft cyan wash), 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 | () => void | Retry handler for a failed bubble; renders a real, focusable Retry button. |
| Parameter | Type | Default | Description |
|---|---|---|---|
message * | ChatMessage | The message model: role drives alignment + surface (USER cyan gradient / ASSISTANT cardAlt plane / SYSTEM centered divider) and carries the body blocks. | |
showHeader | Boolean | true | Grouping control. The list passes !isContinuation so a consecutive same-sender bubble collapses its header and reads as part of the run. |
accentColor | Color | Color.Unspecified | Optional override for the user bubble fill. Unspecified → the default cyan userCard2 → userCard gradient. |
bubbleMaxWidth | Dp | 320.dp | Upper bound on bubble width (widthIn(max=)). Phone cap by default; the list passes a wider value on tablets. |
rememberBubbleShape(tail, radius, tailRadius) | RoundedCornerShape | The bubble silhouette. tail = TOP_RIGHT (user) / TOP_LEFT (assistant); radius default 20.dp, tailRadius default 6.dp. |
Not yet implemented. Nockerl Voice is a dictation/transcription surface with no
chat thread, so no bubble primitive ships on macOS. The signature below is the
intended SwiftUI API (a ViewBuilder content closure), derived from the laws
- the shipped Compose bubble container.
| Parameter | Type | Default | Description |
|---|---|---|---|
role * | BubbleRole | .user → trailing/right; .agent → leading/left; .system → centered. Drives alignment, surface, and the tail corner. | |
group | GroupPosition | .single | Position in a same-sender run, which keeps the tail on the edge bubbles and squares the interior seams. |
delivery | DeliveryState? | nil | Own-message delivery state (.sending / .sent / .failed), shown as a warm receipt under the bubble; never color-alone. |
timestamp | String? | nil | Short time string in the meta line under the bubble. |
content * | () -> Content | A @ViewBuilder body. The bubble is a container; the rich turn is composed on top. |