Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Status legend: ⬜ Todo · ✅ Done
| 17 | Tool | TBD | ⬜ |
| 18 | Suggestion & Suggestion Group | plain & outlined rows; scroll, wrap, column | ✅ |
| 19 | Confirmation | default, approved, rejected | ⬜ |
| 20 | Error state | | ⬜ |
| 20 | Error state | failure card + retry pill; failed assistant turns render it automatically | ✅ |
| 21 | Code block | built-in synchronous highlighter; languages host-extensible | ✅ |
| 22 | Thinking indicator | turning, breathing asterisk + shimmer label; active & settled | ✅ |
| 23 | Shimmer | text only; sweeping highlight, static when settled | ✅ |
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
behind new `code` / `codeInline` typography roles — `withFontFamily()`
no longer touches the mono roles; swap those with
`withCodeFontFamily()`.
- **Error state** — `FlowErrorState` (error glyph, host-written message,
retry pill) and a `FlowErrorPart` message part, with
`onRetry`/`errorTitle`/`retryLabel` threaded through `FlowMessage` and
`FlowThread`.
- **Breaking**: a failed assistant turn no longer recolors its content
into an `errorContainer` bubble — parts keep their normal ink and the
turn closes with an error card (a default one when no `FlowErrorPart`
is present). The user bubble's error treatment is unchanged.
- **Breaking**: migrated from `package:flutter/material.dart` to
`package:material_ui` (Material's home since Flutter 3.47) — no API
changes, but the two Materials are distinct types, so the host app must
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Values come from the Flow UI Figma file. Role names follow Material 3's `ColorSc
| 17 | Tool | TBD | ⬜ |
| 18 | Suggestion & Suggestion Group | plain & outlined rows; scroll, wrap, column | ✅ |
| 19 | Confirmation | default, approved, rejected | ⬜ |
| 20 | Error state | | ⬜ |
| 20 | Error state | failure card + retry pill; failed assistant turns render it automatically | ✅ |
| 21 | Code block | built-in synchronous highlighter; languages host-extensible | ✅ |
| 22 | Thinking indicator | turning, breathing asterisk + shimmer label; active & settled | ✅ |
| 23 | Shimmer | text only; sweeping highlight, static when settled | ✅ |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
| [`FlowThinkingIndicator`](https://flowui.stac.dev/components/thinking-indicator) | Turning, breathing asterisk with a shimmering label |
| [`FlowShimmerText`](https://flowui.stac.dev/components/shimmer-text) | Sweeping text highlight, static once settled |
| [`FlowCodeBlock`](https://flowui.stac.dev/components/code-block) | Fenced code with built-in synchronous highlighting, a header label, and a copy affordance — languages host-extensible |
| [`FlowErrorState`](https://flowui.stac.dev/components/error-state) | Failure card with a host-written message and retry pill — failed turns render it automatically |
| [`FlowMessageActions`](https://flowui.stac.dev/components/message-actions) | Copy / regenerate / edit / feedback row under a message |
| [`FlowComposer`](https://flowui.stac.dev/components/composer) | Multiline input with send/stop, attachments strip, and leading/trailing action slots |
| [`FlowMenu`](https://flowui.stac.dev/components/menu) | Icon-triggered menu with groups, submenus, and toggles — anchored card on desktop, bottom sheet on phones |
Expand Down
120 changes: 120 additions & 0 deletions docs/src/content/docs/components/error-state.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Error state
description: The failure card — an error glyph, a host-written message, and a retry pill that reports intent.
sidebar:
order: 14
---

import FlowDemo from '../../../components/FlowDemo.astro';

`FlowErrorState` is the failure surface: an error glyph and a host-written
explanation on a hairline card, with an optional retry pill. It renders
state and reports one intent — what retry *means* (re-run the turn,
refetch, reconnect, resend) is the host's business. The package ships no
strings, so `title`, `message` and `retryLabel` are all host-localized;
the message announces to assistive tech as a live region, and the pill is
a visible control rather than a hover-revealed action, because hover does
not exist on touch.

## Card

<FlowDemo demo="errorState" variant="card" height={220} title="The full card" />

```dart title="The full anatomy"
FlowErrorState(
title: 'Connection error',
message: 'The API is overloaded right now. Retry in a moment.',
retryLabel: 'Retry',
onRetry: resend,
)
```

## Minimal

Every part is optional: without a `title` the message takes the glyph
row, and a null `onRetry` hides the pill:

<FlowDemo demo="errorState" variant="minimal" height={160} title="Message only" />

```dart title="Just the failure"
FlowErrorState(
message: 'The API is overloaded right now. Retry in a moment.',
)
```

## A failed turn

In a thread the card renders on its own. A `FlowErrorPart` in any turn
becomes this card — and because parts render in order, everything the
turn already delivered keeps its normal ink, with the failure closing the
turn below it. Retry hands the failed message back through
`FlowThread.onRetry`:

<FlowDemo demo="errorState" variant="thread" height={480} title="Partial reply, then the card" />

```dart title="The host contract"
FlowThread(
messages: messages,
errorTitle: 'Connection error',
retryLabel: 'Retry',
// Typically: drop or reset the failed message, re-run the turn.
onRetry: (message) => rerun(message),
)
```

A turn whose status is `FlowMessageStatus.error` but carries no
`FlowErrorPart` still closes with a default card — zero-wiring hosts keep
a visible failure state.

## Not retryable

`retryable: false` on the part suppresses the pill even when the thread
wires `onRetry` — for failures retrying can't fix:

```dart title="A terminal failure"
FlowMessageData(
id: 'a2',
role: FlowMessageRole.assistant,
status: FlowMessageStatus.error,
parts: [
FlowErrorPart(
message: 'This conversation exceeds the context window.',
retryable: false,
),
],
)
```

## Elsewhere

Standalone, the same card serves the other failure surfaces — a thread
that failed to load, a failed send below the composer, or a connection
notice pinned above the input via `FlowChatScreen.aboveComposer`, the
slot that already exists for exactly this:

```dart title="A connection notice above the composer"
FlowChatScreen(
thread: FlowThread(messages: messages),
aboveComposer: offline
? FlowErrorState(
message: 'Connection lost.',
retryLabel: 'Reconnect',
onRetry: reconnect,
)
: null,
composer: FlowComposer(onSend: send),
)
```

## Key API

- `FlowErrorState` — `title`, `message`, `onRetry` (null hides the
pill), `retryLabel` (doubles as the pill's accessible name; null
renders the glyph alone), plus `padding` and `borderRadius` over the
design's 16/14 inset and 12px corner.
- `FlowErrorPart` — `message`, `retryable` (default true); rendered by
`FlowMessage` in part order, skipped in system messages.
- `FlowThread` / `FlowMessage` — `onRetry` (thread-level is handed the
failed `FlowMessageData`), `errorTitle`, `retryLabel`.
- A failed assistant turn keeps its parts in normal ink; the
`errorContainer` recolor now applies only to the user bubble.
2 changes: 1 addition & 1 deletion docs/src/content/docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ elements and the remaining AI states are on the way.
| Tool | <span class="badge-todo">Planned</span> |
| Suggestions | <span class="badge-done">Shipped</span> |
| Confirmation | <span class="badge-todo">Planned</span> |
| Error state | <span class="badge-todo">Planned</span> |
| Error state | <span class="badge-done">Shipped</span> |
| Code block | <span class="badge-done">Shipped</span> |
| Thinking indicator | <span class="badge-done">Shipped</span> |
| Shimmer | <span class="badge-done">Shipped</span> |
Expand Down
1 change: 1 addition & 0 deletions lib/flow_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export 'src/widgets/flow_attachment_preview.dart';
export 'src/widgets/flow_chat_screen.dart';
export 'src/widgets/flow_code_block.dart';
export 'src/widgets/flow_composer.dart';
export 'src/widgets/flow_error_state.dart';
export 'src/widgets/flow_greeting.dart';
export 'src/widgets/flow_menu.dart';
export 'src/widgets/flow_menu_style.dart';
Expand Down
3 changes: 2 additions & 1 deletion lib/src/models/flow_message_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ enum FlowMessageStatus {
/// Settled; renders statically.
complete,

/// Failed; content renders in an error bubble.
/// Failed. An assistant turn keeps its parts in normal ink and closes
/// with an error card; a user bubble recolors to the error container.
error,
}

Expand Down
13 changes: 13 additions & 0 deletions lib/src/models/flow_message_part.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ class FlowCodePart extends FlowMessagePart {
final String? filename;
}

/// A failure surfaced in the turn, rendered by a `FlowErrorState`.
class FlowErrorPart extends FlowMessagePart {
const FlowErrorPart({this.message, this.retryable = true});

/// Host-written and sentence-case. Null renders the card without one —
/// the package ships no strings.
final String? message;

/// False suppresses the retry affordance even when the host wires
/// retry — for failures retrying can't fix.
final bool retryable;
}

/// Host-defined content, rendered through a `FlowCustomPartBuilder`.
class FlowCustomPart extends FlowMessagePart {
const FlowCustomPart({required this.type, this.data});
Expand Down
Loading