Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
024a3a0
chore: check in the example app's platform runners
divyanshub024 Aug 21, 2026
adfe511
feat: require an explicit composer and default the placeholder
divyanshub024 Aug 21, 2026
2fd4ebe
feat: thread reads from the top until it outgrows the viewport
divyanshub024 Aug 21, 2026
34e4b7d
feat: wire the example to a streaming Gemini chat
divyanshub024 Aug 21, 2026
b4f7a04
feat: markdown rendering with streaming frontier, autolinks and inlin…
divyanshub024 Aug 22, 2026
405390b
ci: stub the example's gitignored key file
divyanshub024 Aug 22, 2026
19bbb60
fix: grant the example outbound network access on macOS
divyanshub024 Aug 22, 2026
23dd3d4
fix: declare INTERNET in the example's main Android manifest
divyanshub024 Aug 22, 2026
de0b192
chore: drop the scaffold's stale counter test from the example
divyanshub024 Aug 22, 2026
ed85d23
chore: remove the personal signing team from the example's iOS project
divyanshub024 Aug 22, 2026
7578bdb
Merge branch 'main' into dv/chat-view-api
divyanshub024 Aug 22, 2026
7fe8955
fix: drop the assistant turn when the stream closes with no text
divyanshub024 Aug 22, 2026
add5fea
fix: keep failed turns out of the Gemini request history
divyanshub024 Aug 22, 2026
42ed667
docs: bring the thread page up to date with markdown and top-read
divyanshub024 Aug 22, 2026
e2ca64d
fix: apply review findings across the markdown stack, thread and example
divyanshub024 Aug 22, 2026
dde6b60
style: format the example's link handler
divyanshub024 Aug 22, 2026
ac5d773
fix: left-align markdown entrances and lift the jump button on surface
divyanshub024 Aug 22, 2026
c0f5693
feat: model selector, message actions and a thread messageFooter slot
divyanshub024 Aug 22, 2026
30dd776
docs: update padding and spacing descriptions in FlowThread documenta…
divyanshub024 Aug 22, 2026
300efbd
Merge remote-tracking branch 'origin/main' into dv/chat-view-api
divyanshub024 Aug 22, 2026
08cf414
fix: top-read from the first turn, edge scrollbar and thread breathin…
divyanshub024 Aug 22, 2026
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ jobs:
steps:
- uses: actions/checkout@v7

# The example reads its Gemini key from lib/env.g.dart, which is
# gitignored so a real key can never land in a commit — meaning a
# fresh checkout doesn't have it. Stub it with an empty key, the
# same state a fresh clone runs with (see example/README.md).
- name: Stub the example's key file
if: matrix.app == 'example'
run: |
printf '// CI stub — the real file is gitignored.\nconst String apiKey = %s;\n' "''" \
> example/lib/env.g.dart

- uses: subosito/flutter-action@v2
with:
channel: stable
Expand Down
6 changes: 6 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
"cwd": "playground",
"request": "launch",
"type": "dart"
},
{
"name": "example",
"cwd": "example",
"request": "launch",
"type": "dart"
}
]
}
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ Status legend: ⬜ Todo · ✅ Done
| 22 | Thinking indicator | turning, breathing asterisk + shimmer label; active & settled | ✅ |
| 23 | Shimmer | text only; sweeping highlight, static when settled | ✅ |
| 24 | Pill | removable tool/mode pill for the composer's action row; label auto-drops on phones | ✅ |
| 25 | Markdown | built-in parser + renderer; assistant text parts render it by default; fences compose Code block; tables, links, streaming reveal | ✅ |

### Surfaces

| # | Component | Variants / notes | Status |
|---|-----------|------------------|--------|
| 25 | Chat View | centred 760 rail; zero state (greeting, lifted composer, starters); jump to latest | ✅ |
| 26 | SidePanel | | ⬜ |
| 27 | Modal | | ⬜ |
| 26 | Chat View | centred 760 rail; zero state (greeting, lifted composer, starters); jump to latest | ✅ |
| 27 | SidePanel | | ⬜ |
| 28 | Modal | | ⬜ |
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,53 @@
retry pill) and a `FlowErrorPart` message part, with
`onRetry`/`errorTitle`/`retryLabel` threaded through `FlowMessage` and
`FlowThread`.
- **Markdown** — `FlowMarkdown`, assistant prose typeset by a built-in
parser (no new dependency): headings on the existing type ramp,
emphasis, inline code on the `codeInline` role as a rounded chip
painted under the glyphs — wrapping keeps only the outer corners
rounded — links reporting intent through `onLinkTap` (bare
`https://`, `http://` and `www.` URLs autolink with GFM's trimming
rules), nested lists, quotes, rules, tables with alignment and
overflow scroll, and fenced code rendered by `FlowCodeBlock` —
sharing the `FlowCodePart` copy contract. Streaming input may end
mid-construct and renders gracefully, with the trailing paragraph
revealing character by character.
- **Streaming motion** — markdown reveals through one sequenced
frontier: nothing renders below the animating block, fences, tables
and rules ease in with height and opacity when the frontier reaches
them, and pacing keeps the frontier within the reveal's lag bound on
fast streams. Growth is eased in layout — the revealing paragraph
gains each wrapped line over a beat — so a thread pinned to the
newest message moves continuously instead of stepping a line-height
at a time. Reduced-motion settings render statically.
- **Breaking**: assistant text parts now render as markdown by default.
Hosts whose assistant text is literal pass `markdown: false` on
`FlowThread` or `FlowMessage`; user bubbles and system notices are
unaffected.
- **Keyboard** — taps landing on the chat surface itself (dead space,
the thread, a settled message) now dismiss the keyboard, and scrolling
the thread dismisses it too (`FlowThread.keyboardDismissBehavior`,
default on-drag) — the chat conventions. Interactive children keep
their taps.
- **Thread** — a conversation that still fits its viewport now reads from
the top, the AI-app convention, instead of hugging the composer with
empty space above. Once it outgrows the viewport the thread anchors to
the newest message as before. A new `messageFooter` builder fills each
default message's footer slot (an actions row, a timestamp) without
replacing the whole message the way `messageBuilder` does.
- **Pill** — `FlowPill`, a removable pill showing an enabled tool or mode
in the composer's action row: host-passed icon, label and tooltips,
removal intent on `onRemove`, and a label that auto-drops to the
icon-only form on phones (`showLabel` forces either).
- **Breaking**: `FlowComposer.placeholder` now defaults to
'How can I help you today?' — the one string the package ships. Hosts
that want an empty field must pass an explicit `placeholder: null`;
localized hosts keep passing their own copy.
- **Breaking**: `FlowChatView.composer` is now required — still nullable,
so a read-only surface passes an explicit `composer: null` instead of
omitting it. Building the view with nothing to show at all (no thread,
composer, header, or zero state) now asserts in debug builds rather
than rendering a blank screen.
- **Breaking**: `FlowChatScreen` is renamed to `FlowChatView`. The widget
was never a screen — it is body-only and embeddable, and upcoming
surfaces (side panel, modal) will host it — so the name now follows
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,12 @@ Values come from the Flow UI Figma file. Role names follow Material 3's `ColorSc
| 22 | Thinking indicator | turning, breathing asterisk + shimmer label; active & settled | ✅ |
| 23 | Shimmer | text only; sweeping highlight, static when settled | ✅ |
| 24 | Pill | removable tool/mode pill for the composer's action row; label auto-drops on phones | ✅ |
| 25 | Markdown | built-in parser + renderer; assistant text parts render it by default; fences compose Code block; tables, links, streaming reveal | ✅ |

### Surfaces

| # | Component | Variants / notes | Status |
|---|-----------|------------------|--------|
| 25 | Chat View | centred 760 rail; zero state (greeting, lifted composer, starters); jump to latest | ✅ |
| 26 | SidePanel | | ⬜ |
| 27 | Modal | | ⬜ |
| 26 | Chat View | centred 760 rail; zero state (greeting, lifted composer, starters); jump to latest | ✅ |
| 27 | SidePanel | | ⬜ |
| 28 | Modal | | ⬜ |
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
| Component | What it does |
|---|---|
| [`FlowChatView`](https://flowui.stac.dev/components/chat-view) | The full chat surface: bounded thread over a composer, centred at a readable width, with a zero state (greeting, lifted composer, starters) and a jump-to-latest button |
| [`FlowThread`](https://flowui.stac.dev/components/message-thread) | Scrollable conversation anchored to the newest message |
| [`FlowThread`](https://flowui.stac.dev/components/message-thread) | Scrollable conversation — reads from the top, anchoring to the newest message once it outgrows the viewport |
| [`FlowMessage`](https://flowui.stac.dev/components/message-thread) | One turn — ink-wash user bubble, plain assistant, error bubble, typed content parts |
| [`FlowStreamingText`](https://flowui.stac.dev/components/streaming-text) | Animated text reveal while a reply arrives |
| [`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 |
| [`FlowMarkdown`](https://flowui.stac.dev/components/markdown) | Assistant prose typeset from a built-in parser — headings, emphasis, lists, quotes, tables, links, and fences composing the code block; assistant turns render it by default and it streams gracefully |
| [`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 |
Expand Down
7 changes: 6 additions & 1 deletion docs/src/content/docs/components/chat-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ host keeps the chrome, the background, and the keyboard inset. Pass
`composer: null` for a read-only surface — an archived thread, a shared
transcript.

Taps that land on the surface itself — dead space, the thread, a settled
message — dismiss the keyboard, the chat convention; the composer, links
and buttons keep their taps.

## Jump to latest

Pass the **same** `ScrollController` to the thread and to
Expand All @@ -55,7 +59,8 @@ reach in and attach its own. Null leaves the button out entirely.
- `thread` — usually a `FlowThread`, given the bounded height it needs.
Null renders an empty thread: a conversation nobody has spoken in yet is
a real state of a chat, so the surface stands up on its own.
- `composer` — null renders no input: a read-only surface.
- `composer` — required, but nullable: an explicit `composer: null`
renders no input, a read-only surface.
- `empty` + `greeting` + `suggestions` — the zero state; the host flips
`empty` (typically `messages.isEmpty`).
- `header` — optional full-bleed bar above the thread; `aboveComposer`
Expand Down
3 changes: 3 additions & 0 deletions docs/src/content/docs/components/composer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,8 @@ page shows the full flow, from the "+" menu to the sent message.
preview; `previewCloseTooltip` labels the preview's close button.
- `controller`, `focusNode`, `placeholder` — the field itself; pass your
own `TextEditingController` to prefill or clear the draft.
`placeholder` defaults to 'How can I help you today?' — the one string
the package ships — so localized hosts pass their own copy, and an
explicit null renders no hint.
- `padding`, `borderRadius` — the card's metric overrides (the design's
24px corner by default).
103 changes: 103 additions & 0 deletions docs/src/content/docs/components/markdown.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: Markdown
description: Assistant prose, typeset — headings, emphasis, lists, quotes, tables, links, and fenced code, streaming gracefully.
sidebar:
order: 16
---

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

`FlowMarkdown` typesets what the assistant says. Assistant text parts
render through it **by default** — a thread wired yesterday shows rich
replies today, and `markdown: false` on `FlowThread` or `FlowMessage`
opts a literal-text host back out. User bubbles and system notices always
render plain: what the user typed is a transcription, not prose to
typeset.

The parser is built in — no dependency, synchronous, and written for the
dialect assistants actually emit. Deliberately deferred syntax (images,
task lists, footnotes, inline HTML, setext headings) renders as the
literal text it is.

## The document

Headings sit on the existing type ramp, inline code takes the mono face
on a rounded chip of the faint wash — painted under the glyphs, so a
wrapped chip keeps only its outer corners rounded and the streaming
reveal runs straight through it — quotes step down to the secondary ink
behind the hairline bar, and fenced code renders through
`FlowCodeBlock` — highlighting, header, and copy intent included:

<FlowDemo demo="markdown" variant="document" height={760} title="The typeset reply" />

```dart title="Standalone, outside a thread"
FlowMarkdown(
text: reply,
isStreaming: generating,
onLinkTap: (href) => openInBrowser(href),
onCodeCopy: copyPart,
codeCopyTooltip: 'Copy code',
)
```

## Streaming

Streaming is data, as everywhere: rebuild with a longer `text` and the
trailing paragraph reveals with the same per-character fade plain text
gets, while fences, tables and rules render whole. The parser tolerates
input that ends mid-construct — unclosed emphasis stays literal until its
closer arrives (and restyles without re-fading), a half-typed link shows
its label and hides the URL, an unterminated fence is a code block still
in progress, and a table only appears once its delimiter row lands:

<FlowDemo demo="markdown" variant="streaming" height={640} title="The reveal over styled text" />

## Tables

Column alignment comes from the delimiter row; a table wider than its
column scrolls horizontally inside the message rather than wrapping the
page:

<FlowDemo demo="markdown" variant="tables" height={560} title="Alignment and overflow" />

## Links

Links report intent: the label styles in the accent ink with an
underline, and tapping hands the host the href — the package never
launches URLs. With no `onLinkTap` wired, links render as plain prose
rather than a styled-but-dead affordance. In a thread the callback
carries the message too: `onLinkTap: (message, href) => ...`.

Bare URLs autolink with GFM's rules: `https://`, `http://` and `www.`
(handed to the host with `https://` prepended), trailing punctuation
trimmed, a closing parenthesis kept when the URL's parens balance, and
emails deliberately left literal. Mid-stream, a URL still being typed
links early and its href grows with the text — links are inert while
the reveal runs, so nothing mis-taps.

<FlowDemo demo="markdown" variant="links" height={420} title="Intent, made visible" />

## Fenced code and copy

Every fence synthesizes a `FlowCodePart`, so copy flows through the same
contract code parts use — one `onCodeCopy` handler and one
`copiedCodePart` confirmation serve both. While a fence is still
streaming its copy affordance stays hidden, exactly like a streaming
`FlowCodePart`.

## Key API

- `text` — the markdown source received so far.
- `isStreaming` — animates the trailing text block; tolerant parsing
either way.
- `style` — merged over `bodyLarge` + `onSurface`; headings keep their
scale but follow this style's color.
- `onLinkTap`, `onCodeCopy`, `copiedCodePart`, `codeCopyTooltip` —
intent out, host-localized strings in.
- On `FlowThread` / `FlowMessage`: `markdown` (default true) gates the
assistant-text rendering; `onLinkTap` threads through with the
message-arity shape on the thread.

Prose renders non-selectable `Text.rich`, matching plain assistant text —
wrap the thread in a `SelectionArea` for selection. Fenced code keeps
`FlowCodeBlock`'s own selectable body.
23 changes: 18 additions & 5 deletions docs/src/content/docs/components/message-thread.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import FlowDemo from '../../../components/FlowDemo.astro';

`FlowThread` renders a conversation from plain message data — user,
assistant, and system roles — and `FlowMessage` renders a single turn: an
ink-wash bubble for the user, plain text on the page for the assistant.
Neither knows where the messages came from.
ink-wash bubble for the user, typeset prose on the page for the assistant
(assistant text renders as [markdown](/components/markdown/) by default;
`markdown: false` opts a literal-text host out). Neither knows where the
messages came from.

A conversation that still fits its viewport reads from the top, the
AI-app convention; once it outgrows the viewport the thread anchors to
the newest message.

## A conversation

Expand Down Expand Up @@ -100,13 +106,20 @@ card, a chart) without the library knowing what it is.

- `FlowThread` — the scrolling conversation; give it a `ScrollController`
to pair with `FlowChatView`'s jump-to-latest. `padding` (the design's
16) and `itemSpacing` (32) override the metrics; `messageBuilder`
16 at the sides, 40 vertically) and `itemSpacing` (32) override the
metrics; `messageBuilder`
swaps the default `FlowMessage` per turn; `thinkingLabel`,
`charactersPerSecond`, and `previewCloseTooltip` forward to every
message.
message; `markdown` (default true) and `onLinkTap: (message, href)`
gate and wire the assistant markdown; `keyboardDismissBehavior`
(default on-drag) dismisses the keyboard as the thread scrolls;
`messageFooter` builds each message's footer slot (an actions row, a
timestamp) without replacing the default message the way
`messageBuilder` does.
- `FlowMessage` — one turn; accepts a `FlowCustomPartBuilder`, an
`onAttachmentTap` override, `leading` and `footer` slots (an avatar,
the action row), a `textStyle`, and the user bubble's overrides —
the action row), a `textStyle`, the same `markdown` gate and a
single-arity `onLinkTap`, and the user bubble's overrides —
`maxBubbleWidthFraction` (0.75), `bubbleRadius`, `bubblePadding`.
- `FlowMessageData` — `id`, `role`, `parts`, `status`, and an optional
`timestamp`; the `.text` constructor covers the plain case.
1 change: 1 addition & 0 deletions docs/src/content/docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ elements and the remaining AI states are on the way.
| Confirmation | <span class="badge-todo">Planned</span> |
| Error state | <span class="badge-done">Shipped</span> |
| Code block | <span class="badge-done">Shipped</span> |
| Markdown | <span class="badge-done">Shipped</span> |
| Thinking indicator | <span class="badge-done">Shipped</span> |
| Shimmer | <span class="badge-done">Shipped</span> |
| Pill | <span class="badge-done">Shipped</span> |
Expand Down
52 changes: 50 additions & 2 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Your Gemini API key lives here — created by hand, never committed.
lib/env.g.dart

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
build/
pubspec.lock
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

# Widget Preview related
.widget_preview/
Loading