Skip to content
Open
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
40 changes: 32 additions & 8 deletions skills/stream-flutter/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,29 @@ Before any tool call, decide the **track** from the user's input alone - no prob
| "Install Stream packages", "set up Stream in Flutter", "wire auth/token" with no broader feature request | **D - Bootstrap / setup** |
| Bare `/stream-flutter` with no args | List the tracks briefly and wait |

### Styling-depth flag (orthogonal to Tracks A/B/C/D)

If the request carries a **target appearance** — an attached screenshot, a Figma frame, or "make it look like WhatsApp / iMessage / Telegram / Slack / <app>" — then **before** feature work (Track A or B), **first run the strategy decision below** (components vs custom): if it lands on components, run [`design-matching.md`](design-matching.md); if it lands on custom (livestream / overlay / bespoke), run [`custom-ui.md`](custom-ui.md) instead — same region-by-region rigor, different mechanism. The rest of this paragraph is the components case (the common one). A reference design is a checklist of regions (header, composer buttons, where the timestamp + read receipts sit, bubble shape/tail, date separators, attachments...), and most of them differ from Stream's defaults **structurally**, not just by color. Do **not** stop at the wallpaper and bubble color — that is the known failure mode. Decompose every region first (capturing its **dimensions**, not just colors), then route each to one of **two axes**: (1) **Theming** — `StreamTheme` (a `ThemeExtension` on `MaterialApp.theme.extensions`) owns design foundations (spacing / radius / typography / colors) **and** all fine-grained/leaf styling including the whole message row (`messageItemTheme` bubble/text/attachment/metadata, `reactionsTheme`, `reactionPickerTheme`, `avatarTheme`, `textInputTheme`, `mediaViewerTheme`, …), while `StreamChatThemeData` (passed to `StreamChat(themeData:)`) owns only the chat composite-widget slots (the three headers, message-list background/wallpaper, channel-list item, quoted message, thread tile, poll themes); (2) **Widget replacement** — the component factory, populated via **core named slots** on `StreamComponentBuilders(...)` (~48 leaf slots: `messageBubble`, `messageText`, `reactions`, `reactionPicker`, `mediaViewer`, `jumpToUnreadButton`, `textInput`, `avatar`, …) **and** **chat slots** via `streamChatComponentBuilders(...) → extensions:` (~30 composite slots: `messageItem`, `messageComposer` + sub-slots, `channelListItem`, per-attachment builders, …), passed to `StreamChat(componentBuilders: StreamComponentBuilders(...))` (global) or `StreamComponentFactory(builders: …, child: …)` (scoped); plus per-widget builders (`messageBuilder`, `itemBuilder`) for a single instance. Recurring traps the doc guards against: (a) overriding a **composite widget** (`messageItem` builder, `messageComposer` builder, `messageBuilder` on `StreamMessageListView`) silently drops the sub-features the default rendered — the incoming-message avatar, grouping, reactions, replies, status, or the send/voice/edit/slow-mode button — unless you read the default's `build()` and reproduce them (grouping state is available inside a custom `messageItem` via `StreamMessageLayout.of(context)`); (b) **model-driven title logic** must live in one shared helper used by both the channel list `itemBuilder` and the header, so the two surfaces cannot diverge; (c) route deliberately — bubble padding / color / shape live on **`StreamTheme`** (`StreamTheme.messageItemTheme.bubble`), not on `StreamChatThemeData`, while structural changes (send button outside the field, metadata inside the bubble, a bubble tail, a Slack-style flat row) need **widget replacement** (component factory or a per-widget builder). The match is **not done until you run, seed data that triggers every region, compare region-by-region against the reference on the real navigation path, and iterate** ([`design-matching.md`](design-matching.md) Step 5), deleting any throwaway verification scaffold — the UI must be as close to the reference as possible, not approximately like it. **Implement every region, the composer included** — never deliver a partial match with the rest labelled "known cosmetic gaps"; a region left at the SDK default is a FAIL, not a footnote. And **work in batches**: ground the pinned SDK version + local checkout once, read the source you need in one pass, implement all regions, then verify once on hot reload — don't rebuild-and-screenshot after every small edit.

### Chat only: pick the UI strategy first (before any code)

Stream Chat ships **two layers**, and choosing between them is an **architecture decision that dwarfs any styling choice** — getting it wrong wastes a day either way. So decide deliberately, and **default to the pre-built components**:

| Strategy | Use when the design is... | Mechanism | Runbook |
|---|---|---|---|
| **Pre-built UI components** (`stream_chat_flutter`) — **the default** | A messenger: bubbles, or a channel list → conversation, or per-message avatar/timestamp/receipts/reactions/attachments. Social, marketplace, workplace, support, DMs. "Make it look like WhatsApp / iMessage / Telegram / Slack." | Customize via the two axes — theming (`StreamTheme` for foundations + the message row/leaf widgets, `StreamChatThemeData` for the chat composite widgets) + widget replacement (the component factory `StreamComponentBuilders` / `streamChatComponentBuilders` / `StreamComponentFactory`, plus per-widget builders) | [`design-matching.md`](design-matching.md) |
| **Custom UI on `stream_chat_flutter_core`** — the exception | Not a messenger: a flat bubble-less author-inline feed, an overlay/ticker on video, high-volume ephemeral livestream chat (Twitch / YouTube / Kalshi), live shopping, or anonymous/guest read-only viewers vastly outnumbering posters (→ `livestream` channel type). Every message rendered identically; bespoke app chrome around it. | Build your own widgets on the headless `stream_chat_flutter_core` controllers (+ the low-level client); **no** `stream_chat_flutter` widgets | [`custom-ui.md`](custom-ui.md) |

**Lean hard toward components.** They're built to be *customized*, and the litmus test is: *if the two axes — theming (`StreamTheme` + `StreamChatThemeData`) + a few component-builder slots — could get there, it's a components job* — even strong messenger reskins. Pick **custom only** when matching the design would mean replacing the message row, composer, header, AND list all at once — i.e. you'd be using the SDK purely as a data source, not for any of its widgets. Over-choosing custom (rebuilding a worse messenger by hand, losing avatars/grouping/reactions/threads/attachments/typing/receipts/pickers) is the common, expensive mistake; over-choosing components costs a few hours of fighting layout. **When unsure, build the components version first** — it's faster to confirm-or-reject. And treat **"livestream" / "live-shopping" as signals, not labels**: they point to custom only when the *shape* matches (flat identical rows, overlay/ticker, high volume, read-only viewers) — a livestream app that wants a normal bubble/channel-list chat panel stays on **components**. This is the full decision rubric; [`custom-ui.md`](custom-ui.md) is the build runbook you follow *after* the decision lands on custom, not a doc you open to decide.

**Workplace / Slack-style hybrid is a components job too.** A Slack/Teams/Discord surface is a channel list + message list + composer, so it stays on the pre-built components — but two things differ from a messenger and must be matched, not punted: (1) the **message row** is flat and left-aligned (avatar-top rounded-square, an author·custom-status·timestamp header line, body, **bottom reaction pills**, and a **thread-reply summary**), with **no incoming/outgoing bubble split** — reproduce it by overriding the `messageItem` component builder (a composite slot — reproduce its sub-features); (2) the **header** and chrome are custom. The full workplace archetype is in [`design-matching.md`](design-matching.md). Workplace apps are also **thread-first**, so wire the thread-reply summary and thread screen.

The strategy also picks the **channel type and permission model** (e.g. `messaging` membership-gated for social/marketplace vs `livestream` public + anonymous viewers) — see [`RULES.md`](RULES.md) → "Surface permission prerequisites proactively", and Step 0.5 → "Permissions awareness". Decide both axes together.

If it's genuinely unclear, ask one question:

> Does this chat look like a standard messenger (channel list + bubbles), or a bespoke surface like livestream/overlay chat? It decides whether we customize the pre-built components or build custom UI on `stream_chat_flutter_core`.

### Disambiguation flow

If the request is ambiguous between **build/integrate** and **reference lookup**, ask one short question and wait:
Expand All @@ -59,7 +82,7 @@ If the request is ambiguous between **build/integrate** and **reference lookup**

### After classification

- **Tracks A, B, D** -> run **Project signals** once per session, then continue in [`builder.md`](builder.md) and [`sdk.md`](sdk.md).
- **Tracks A, B, D** -> run **Project signals** once per session, then continue in [`builder.md`](builder.md) and [`sdk.md`](sdk.md). If the styling-depth flag was raised, run the design-match phase alongside per the UI-strategy decision — [`design-matching.md`](design-matching.md) for the pre-built components (the common case) or [`custom-ui.md`](custom-ui.md) for a bespoke `stream_chat_flutter_core` surface (design-match rigor is a mandatory phase, not an optional add-on).
- **Track C** -> skip the probe if the product + package are explicit. Only run it on demand if the SDK layer is ambiguous.

---
Expand Down Expand Up @@ -265,12 +288,13 @@ Only proceed with Chat work once the project resolves a v10 (`^10.0.0`) dependen

## Module map

| Track | Module(s) |
| --------------------- | -------------------------------------------------------------------------- |
| A - New app | [`builder.md`](builder.md) + [`sdk.md`](sdk.md) + relevant reference files |
| B - Existing app | [`builder.md`](builder.md) + [`sdk.md`](sdk.md) + relevant reference files |
| C - Reference lookup | [`sdk.md`](sdk.md) + relevant reference files |
| D - Bootstrap / setup | [`builder.md`](builder.md) + [`sdk.md`](sdk.md) |
| Track | Module(s) |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| A - New app | [`builder.md`](builder.md) + [`sdk.md`](sdk.md) + relevant reference files |
| B - Existing app | [`builder.md`](builder.md) + [`sdk.md`](sdk.md) + relevant reference files |
| C - Reference lookup | [`sdk.md`](sdk.md) + relevant reference files |
| D - Bootstrap / setup | [`builder.md`](builder.md) + [`sdk.md`](sdk.md) |
| Styling-depth flag (screenshot/Figma/"look like") | Pick the UI strategy first (Step 0). Components → [`design-matching.md`](design-matching.md); bespoke/livestream/overlay → [`custom-ui.md`](custom-ui.md). Run **before** feature work on Track A/B; region-by-region rigor + verification loop either way |

> **Feeds note (Track A/B):** Use Twitter-style UI by default. Only deviate if the user explicitly requests a different style (e.g., "Instagram grid", "Reddit-style votes").

Expand Down Expand Up @@ -307,7 +331,7 @@ Additional Stream product coverage should stay in this naming family instead of
| Phase | Name | What you do |
| ------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A1** | Detect | Run **Project signals**. If there is no Flutter app yet, tell the user to run `flutter create my_app` first. |
| **A2** | Choose lane | Confirm package choice: `stream_chat_flutter` (pre-built UI, fastest), `stream_chat_flutter_core` (custom UI), `stream_video_flutter` (video/livestream), or `stream_feed` / `stream_feed_flutter_core` (activity feeds, no pre-built UI). For Feeds, default to Twitter-style UI. |
| **A2** | Choose lane | Confirm package choice: `stream_chat_flutter` (pre-built UI, fastest), `stream_chat_flutter_core` (custom UI), `stream_video_flutter` (video/livestream), or `stream_feed` / `stream_feed_flutter_core` (activity feeds, no pre-built UI). For Chat, this is the same components-vs-custom call as Step 0's "pick the UI strategy first" — **default hard to `stream_chat_flutter`**; pick core only for genuinely non-messenger surfaces ([`custom-ui.md`](custom-ui.md)). For Feeds, default to Twitter-style UI. |
| **A3** | Install + wire | Follow [`builder.md`](builder.md) + [`sdk.md`](sdk.md), then load only the needed reference files. |
| **A4** | Verify | Confirm `flutter pub get` succeeds, client connects, and first screen renders. If the app discovers/joins channels or uses guest auth, re-state the permission prerequisite (Step 0.5 → Permissions awareness) so those flows don't 403 on first run. |

Expand Down
Loading