Connects is the catalog, then the connect flow, then health.
Apps your assistants run on — browse what's connectable, connect it (OAuth or a pasted key), see it healthy, and know the moment it needs you.
The widths drawn here are samples of a fluid
continuum, not the only states that exist — content (spacing, type, gaps, card/column widths)
scales continuously between them, each clamp reading a named container. Structure (rail,
row↔stack, grid columns) steps only at sm 640 · md 768 ·
lg 1024; radius, hairlines, and pills never scale. Kit: design-canon.md U8.
Reading order in this document: § Desktop
journey first — every desktop (and 768 tablet) frame, full flow, journey order — then
§ Mobile journey — 375 below it — every 375 frame, same order, laid out side
by side as a wrapping grid (never a tall single-file column of phone screens). Two sections,
never interleaved — a review-document convention only (this repo's design-canon.md
§ Mockup review reading order). Mobile 375 is still designed and scored FIRST in the /design
ritual (kit canon U5/U8) — the two rules never conflate.
Connect an app once; every assistant that needs it can use it. Amber = needs your attention.
ConnectsPage.tsx
ConnectionStatusButton control every real app row uses — one control species everywhere. Their menus open the same as any connected row's; Disconnect (and, for AI, Switch account) render GREYED with a signed reason instead of being omitted or drawn as a dead chevron-less chip — the earlier "no status pill, no Reconnect/Disconnect" note above is retired.assistant-lifecycle.html#sec-5b) draws the Storage menu; this C7 note is the Connects-page source for all three lines. Pixel evidence: worktree .claude/tmp/s2-screenshots/s2-ai-menu-* and s2-storage-menu-*.BYOK_AI_HIDDEN (#2514) — no live BYOK-swap entry point yet; lives inside the AI row's own menu as a greyed "Switch account…" item ("MystFlo AI is the default option at the moment.") rather than a separate action, and returns live the moment the flag flips (corrected 2026-08-11).badge-label--info pill is retired, see the Google row above for the reference instance).Connect an app once; every assistant that needs it can use it.
supportedMethods in config/connections.ts — OAuth, API key, or WhatsApp's own embedded-signup + manual-fallback pair. Each shape draws ONE real exemplar (the actual shipped copy, not placeholder text) and lists every app that shares it — active and coming-soon — instead of redrawing near-identical forms per app.ApiKeyForm · PROVIDER_GUIDES.whatsapp
contentWidth="wide"; mobile collapses the how-to steps behind a tap (prereq list stays open — that visibility is the point of #3030).contentWidth="wide" (sheet-slide-panel.md #2193/#2194 — sanctioned for "split form+preview") drops the 1120px reading-column cap so the sheet body runs to ~920px. Body splits reference LEFT 38% / active-work RIGHT, matching the locked split axis (2026-07-26: LEFT = reference/page pane, RIGHT = active work). This is NOT the AssistantModalShell page-slide mechanic — no separate real page renders, it's a plain two-column body inside one `ModalWrapper` sheet.<details> disclosure (closed by default) — matches 3 existing call sites (MetaSetupChecklistStep, DataPrivacySection, AssistantSummaryCard.post). The "Before you start" prereq list stays always-open — that visibility is the #3030 audit's specific fix, not something to re-hide.lg (mobile), the split collapses to a single stacked column — no room for two columns (2026-06-14 lock, applies to every 60/40 usage in this codebase) — same collapsed-how-to treatment carries over.ErrorDisplayWithAI card.ErrorDisplayWithAI · PR #3103 / issue #3113
errorFieldId === 'phone_number_id' in ApiKeyForm.tsx) — number, token, and WABA ID keep their normal borders. Before #3103 every field read the shared error string and went red together; that all-red state was the bug — never draw or build it.ErrorDisplayWithAI with the form's friendlyOverride: title "Number already connected", the same one-line message, one "How to fix" step, and — because supportContact: true — the "Message us" link (opens WhatsApp and auto-sends structured error context to the team) with its explainer line. "Try Again" renders because the form passes onRetry; it clears the error and re-submits.ErrorDisplayWithAI card, directly under the fields — identical to desktop, not just annotated.OAuthForm.tsx · AUTO_METHOD_LABELS
supportedMethods.apiKey: false → hasManual false). The Auto tab is always the default.MystFlo will be able to read and edit only the Sheets and Docs you create or pick — never your whole Drive.
EmbeddedSignupForm.tsx
ConnectionModal.tsx always renders the tab strip (lit-underline idiom, Auto default with a quiet "· Recommended"), one self-describing subtitle per tab. The form's own "Prefer manual setup?" link is suppressed in this host (hideManualFallback), so the tab is the only way across. Corrected against shipped code at merge time — the pre-merge draft of this frame claimed the tabs had been replaced by a text link, which was never true of the shipped modal.
Connects → "Connect" on the WhatsApp card
│
sheet opens — AUTO tab is default
│
┌────────── AUTO (3a) ────┴────────── MANUAL (3b) ─────────┐
▼ ▼
has a Facebook account? 7-item prerequisite gate
no → "Create one free" (before any fields — incl.
│ yes System User token / Never)
▼ │
[Connect with Meta] ──► Meta popup ▼
Business Portfolio, WABA, and paste 4 credentials → Save
number can be CREATED in-popup │
(coexistence offered if number live Graph validation
is on the WhatsApp Business app) (EF whatsapp?action=connect)
│ ┌────────────┼─────────────┐
▼ ▼ ▼ ▼
auth code → server-side exchange 409 number 400 invalid success
(same EF, action=connect) already credentials │
│ connected │ │
▼ inline field error state + ▼
Connected ✓ error retry Connected ✓
Meta's Embedded Signup handles everything — no manual setup needed
Requires a Facebook account. Don't have one? Create one free
EmbeddedSignupForm.tsx)
Follow the steps in the popup to connect your WhatsApp Business account
loading-sdk/exchanging — same spinner shell, only the headline text changes ("Preparing Meta login…" / "Connecting your WhatsApp account…").Something went wrong. Please try again.
Or enter credentials manually
Or use Meta's onboarding page ↗
hideManualFallback, which only hides the idle-state link) — both land on 3b.Meta requires your WhatsApp Business App number to have at least 7 days of real conversations before it can connect to platforms like MystFlo. This is a Meta security requirement — not a MystFlo limitation.
Already have API credentials? Enter them manually
ErrorDisplayWithAI card below the fields, identical stack to desktop f3b (#3113). The healthy state is the same frame without .fld.err/.err-card; see the desktop frame below.MystFlo will be able to read and edit only the Sheets and Docs you create or pick — never your whole Drive.
PROVIDER_GUIDES.stripe
PROVIDER_GUIDES entry — only the URL, steps, warning, and key format differ).You'll need a Stripe account. Sign up or log in first.
Connect an app once; every assistant that needs it can use it. Amber = needs your attention.
ConnectionStatusButton + T336's trigger-color invariant — corrected 2026-07-23, was previously drawn monochrome here); dropdown holds only Disconnect.getGoogleScopeLabels()) — no colored badge.assistant-flows/assistant-lifecycle.html sec-5b/sec-9d) lists the SAME underlying connections by ROLE instead (Communication / Payment / Storage / AI Model), scoped to what that one assistant uses — its badge is the picker for role-level choices (which provider, which resource), while R35's split-doorway sends any "fix this app" action (Reconnect, or — post-activation — "use a different account") HERE, to this exact row. The two surfaces never duplicate each other's job: role vs. resource choice lives on the assistant; account-level health, scopes, and disconnect live here.Connect an app once; every assistant that needs it can use it. Amber = needs your attention.
DrillOver shell Billing already ships (760px centered content column) — matching this frame's own "mirrors mobile exactly" note and the canon "slide-over as the real page" rule; the earlier drawn 560px panel cap was a drawing artifact, not a spec.RECOVERY_MESSAGES · OAUTH_ERROR_MESSAGES · Continue-Activation block, ConnectsPage.tsx
.banner pattern (pulse.html).?focus=whatsapp&reason=credential_expired) or failed-run redirect; page auto-scrolls to the named row and rings it ~10s (amber inset + tint, not a boxed card).DriveFileScopeBanner): pre-T340 Google connections hold legacy Drive scopes but not drive.file — every picker pick would silently 403. Same .banner.warn strip as the others; NO Dismiss at any width (functional gate, PRD §2.4). Detail in 7b below.sm 640.DriveFileScopeBanner.tsx (shipped, restyle target)
ConnectionStatusButton; the banner never replaces or duplicates it. · NEW (PRD debt) — banner renders iff a status=connected Google row exists AND its stored scopes lack drive.file (googleScopes.ts:66 googleNeedsDriveFileReconnect). A legacy Drive scope is NOT required: hasDriveFileScope returns false for null/empty/absent scopes, so an unpopulated row also shows the banner — fail-loud by design, since we cannot confirm coverage. Post-T340 connections never see it. · NEW (PRD debt) — non-dismissible: no dismiss affordance at any tier; unmounts only when useGoogleScopeStatus re-checks after a successful reconnect. · NEW (PRD debt) — copy is context-keyed to exactly three strings (connects / wizard / knowledge), verbatim from the component; no fourth context without a PRD row..banner .cta treatment (amber tint, verb Reconnect Google) — NOT an embedded ConnectionStatusButton. REQ-C2-01 scopes itself to connection ROWS (Connections row, Wizard Step 1, detail-modal Connect tab); §2.4's T347 carve-out sanctions the banner as an alert surface with "a single 'Reconnect Google' CTA". Embedding the primitive would put two status controls for one connection on one page (the row below already carries it) — the "three signals for one thing" bug §2.3.1 exists to kill — and would drag in the primitive's dropdown/label contract (REQ-C2-03/04), both wrong inside a banner. The two-verb ledger still binds the CTA label: Reconnect + provider name, exactly §2.4's own copy..banner.warn strip renders on Brain (brain.html f26, context knowledge: "Reconnect Google to link existing documents") and in the wizard Personalize step (context wizard: "Reconnect Google to continue activation"). The wizard's artifact (assistant-flows) does not yet depict this state — flagged as mockup debt, not silently skipped. The old boxed card (rounded-xl amber glass + 40px AlertTriangle tile + outlined button) is RETIRED — this frame is the contract.IncrementalScopeModal (state #9 flow), requesting DRIVE_FILE_SCOPE. Correction (this pass, #2889): PRD §2.3.7 / REQ-C2 table say this modal's button keeps the verb "Grant access" — the shipped component (IncrementalScopeModal.tsx:190) actually renders "Allow access". Genuine spec/code drift, not a typo here: the mockup below draws the shipped text; the PRD row is stale and should be corrected to match code, not the reverse. Full sheet + hand-off sequence: 7c below.IncrementalScopeModal.tsx)We need one more permission from Google to enable this feature.
Permission needed
This permission will be added to your existing Google connection. You can revoke access anytime from your Connects page.
We need one more permission from Google to enable this feature.
Permission needed
This permission will be added to your existing Google connection. You can revoke access anytime from your Connects page.
IncrementalScopeModal.tsx (shipped, not previously drawn anywhere)
ConnectionStatusButton throughout this whole flow, unaffected by the modal or the redirect. · NEW (PRD debt) — sheet presentation, title Permission needed for {featureName}, one row per requested scope (this call site always exactly one: DRIVE_FILE_SCOPE). · NEW (PRD debt) — confirming state disables both actions and swaps the primary to a spinner + "Connecting…"; no PRD row specifies this UI, only the underlying OAuth mechanics (§7). · NEW (PRD debt) — "Allow access" is a full top-level redirect, never a popup; §7 step 2/3 confirm the mechanism, no AC covers the UI presentation of the hand-off. · NEW (PRD debt) — return path is a page reload via oauth-callback → /connects?status=success, not the component's own onGranted prop (never invoked inside IncrementalScopeModal's own success path — it exists on the interface but this call site's redirect bypasses it)..flow-card.sheet-card/.flow-head/.flow-body chrome already established in frame 3's Connect-flow sheet — never a new sheet shape for this frame.DriveFileScopeBanner.tsx's own string tables. Wizard context artifact remains mockup debt per 7b.#4K2): gated on OPEN #2590 — the build ships the plain status chip until that discriminator is decided; the drawn chip is the target state, not current drift.We need one more permission from Google to enable this feature.
Permission needed
This permission will be added to your existing Google connection. You can revoke access anytime from your Connects page.
flex-col sm:flex-row). Close ✕ sits top-right of the title row (the shipped ModalWrapper band, simplified here to the title row — no separate sticky band drawn at this scale).requiredScopes=[DRIVE_FILE_SCOPE]) — the list is variable-length in the component (this is why it's a sheet, not a dialog) but every Drive-reconnect call site requests the single drive.file scope.We need one more permission from Google to enable this feature.
Permission needed
This permission will be added to your existing Google connection. You can revoke access anytime from your Connects page.
window.location.href), not a MystFlo
surface. Out of scope for this artifact.
handleRequestPermission calls setIsRequesting(true), then window.location.href = authorizationUrl in the same handler; no popup is ever attempted, no server round-trip is shown. Both buttons disable together; the primary swaps to the Shield glyph (drawn here with the shared .step-ic spinner idiom) + "Connecting…". Holds identically at every tier drawn in 7c — only the button layout (stacked vs row) differs across breakpoints, not this state.oauth-callback. That screen is Google's product surface, not MystFlo's; it isn't drawn here on purpose.oauth-callback redirects to /connects?status=success; the page remounts, useGoogleScopeStatus re-checks, finds drive.file present, and the banner unmounts. The Google row does not change — it was already emerald Connected before the reconnect (the missing scope only broke file-picker calls, never the row's own health signal, frame 7).ProviderPickerModal
ProviderPickerList row shape shared with ConnectionModal step 1.ConnectionStatusButton verbs: Connected (dropdown only) · Connect.Behind the picker: the wizard's app-role list.
Select the AI model for your assistant
Select the AI model for your assistant