Skip to content

perf(app): reduce startup work — remove macOS calendar + defer providers#6597

Open
mdmohsin7 wants to merge 17 commits intomainfrom
perf/app-startup-optimizations
Open

perf(app): reduce startup work — remove macOS calendar + defer providers#6597
mdmohsin7 wants to merge 17 commits intomainfrom
perf/app-startup-optimizations

Conversation

@mdmohsin7
Copy link
Copy Markdown
Member

@mdmohsin7 mdmohsin7 commented Apr 13, 2026

Summary

Two related cleanups to reduce cold-start work in the Flutter app.

1. Remove macOS-only calendar frontend code (8 commits)
The EventKit-based calendar feature was macOS-only; the macOS Flutter desktop app has been removed, so all calendar client code on mobile was dead. CalendarProvider._init() was already a no-op stub, but the rest of the class was still wired up and instantiated eagerly at app launch.

Removed:

  • providers/calendar_provider.dart
  • services/calendar_service.dart
  • pages/settings/calendar_settings_page.dart (orphaned — no nav entry)
  • backend/http/api/calendar_meetings.dart
  • backend/schema/calendar_meeting_context.dart
  • 6 calendar preference keys
  • Dead CalendarProvider? field on CaptureProvider
  • Unused calendarTypeChanged mixpanel event

Preserved: Google Calendar OAuth integration (separate BaseIntegrationService) and the backend /calendar/meetings router (kept intact for future use).

2. Scope + defer non-critical providers at startup (6 commits)
Root MultiProvider was eagerly instantiating ~25 providers. Rescoped based on actual usage:

Moved to screen scope (only one subtree consumes them):

  • DeveloperModeProvider + McpProviderDeveloperSettingsPage
  • AiAppGeneratorProviderAiAppGeneratorPage
  • VoiceRecorderProviderChatPage (disk I/O checkPendingRecording() now fires on chat mount instead of app launch)
  • PaymentMethodProviderPaymentsPage; AddAppPage uses a local instance for its one-shot check

Marked lazy: true at root (cross-screen but no init side-effects):

  • ActionItemsProvider, GoalsProvider, TaskIntegrationProvider, IntegrationProvider, FolderProvider, PhoneCallProvider

Net cold-start impact

  • Eager root providers: ~25 → ~14
  • Removed 3 startup side-effects: GoalsProvider.init() API fetch, VoiceRecorderProvider disk scan, DeveloperModeProvider.initialize().
  • ActionItemsProvider still preloads 100 action items in its constructor — left as a separate follow-up (requires gating on a tasks-enabled preference).

Test plan

  • Cold start app on iOS — reaches home screen; no crashes; no missing-provider errors
  • Cold start app on Android — same
  • Open developer settings → MCP keys list loads; developer mode toggles work
  • Open AI App Generator → prompt + samples load
  • Open chat → pending voice recording is recovered if one exists; recording flow works
  • Open Payments settings → Stripe/PayPal connection status loads
  • Publish a paid app via AddAppPage → "start earning" prompt shows when no payment method connected
  • Verify Google Calendar integration still connects/disconnects from integrations page
  • Verify no regressions on goals widget, action items, folders, integrations list, phone calls flow

The EventKit-based calendar feature was macOS-only and the macOS
Flutter desktop app has been removed.
EventKit bridge for the removed macOS desktop app.
No remaining navigation entry points after macOS desktop removal.
Only the removed macOS calendar provider called this endpoint.
Backend /calendar/meetings router is kept intact for future use.
Only referenced by the removed calendar_meetings HTTP client.
Drops calendarId, calendarType, calendarIntegrationEnabled,
showEventsWithNoParticipants, showMeetingsInMenuBar, and
enabledCalendarIds — all exclusive to the removed EventKit flow.
…vider

The field was declared but never read or assigned anywhere.
…ettings page

Both providers are only consumed by the developer settings flow.
Moving them out of the root MultiProvider avoids eager construction
and API work on app launch for users who never open developer settings.
Single-page provider; no reason to create it at app launch.
Only consumed by the chat page and its voice recorder widget.
checkPendingRecording() (disk I/O) now fires on chat page mount
instead of on every cold start.
Scoped to the payments flow so the provider is not constructed at
app launch for users who never visit payments.
…AddAppPage

After publishing a paid app, the page does a single
getPaymentMethodsStatus() + activeMethod read to decide whether to
show the "start earning" prompt. A local instance avoids needing
the provider in the widget tree and allows the root registration
to be removed.
- Remove CalendarProvider wiring (macOS-only, now deleted).
- Remove 5 providers now scoped to their consuming pages:
  DeveloperModeProvider, McpProvider, AiAppGeneratorProvider,
  VoiceRecorderProvider, PaymentMethodProvider.
- Mark 6 providers lazy so they construct on first read instead
  of app launch: ActionItemsProvider, GoalsProvider,
  TaskIntegrationProvider, IntegrationProvider, FolderProvider,
  PhoneCallProvider.

Net effect: cold-start MultiProvider drops from ~25 eager
providers to ~14, and removes startup side-effects including
GoalsProvider.init() API fetch, VoiceRecorderProvider disk scan,
and DeveloperModeProvider.initialize().
@mdmohsin7 mdmohsin7 marked this pull request as ready for review April 14, 2026 15:30
@mdmohsin7
Copy link
Copy Markdown
Member Author

@greptile-apps review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 14, 2026

Greptile Summary

This PR does two things: removes ~900 lines of dead macOS-only EventKit calendar code (provider, service, settings page, API client, schema, 6 preference keys), and reduces cold-start work by moving 4 providers to screen scope and marking 8 root providers lazy: true, cutting eager root providers from ~25 to ~14.

All provider migrations were verified to be safe: DeveloperModeProvider is only consumed inside DeveloperSettingsPage; VoiceRecorderProvider is only consumed inside the chat page subtree; AiAppGeneratorProvider correctly re-wires to AppProvider via a manual setAppProvider() call in initState; and PaymentMethodProvider remains in the root MultiProvider (lazy), so navigator-pushed payment sub-pages (StripeConnectSetup, PaypalSetupPage) can still access it without a ProviderNotFoundException.

Confidence Score: 5/5

  • This PR is safe to merge — all provider scope changes are correctly bounded, no dangling references remain, and the calendar cleanup is complete.
  • All 13 changed files were reviewed. No P1/P0 issues were found. The provider migrations were each verified by tracing usages across the codebase: DeveloperModeProvider, VoiceRecorderProvider, and AiAppGeneratorProvider are consumed only within the pages that now scope them. PaymentMethodProvider stays at the root (lazy), keeping payment sub-pages crash-free. Calendar deletion is complete with no lingering imports. The add_app.dart one-shot PaymentMethodProvider instance is an explicitly documented choice and is functionally correct. All remaining findings are P2 or better.
  • No files require special attention.

Important Files Changed

Filename Overview
app/lib/main.dart Root MultiProvider trimmed from ~25 to ~14 eager providers; DeveloperModeProvider/McpProvider/AiAppGeneratorProvider/VoiceRecorderProvider removed from root; ActionItemsProvider, GoalsProvider, TaskIntegrationProvider, IntegrationProvider, FolderProvider, McpProvider, PaymentMethodProvider, PhoneCallProvider marked lazy:true. CalendarProvider and its import removed.
app/lib/pages/chat/page.dart ChatPage converted from StatefulWidget to StatelessWidget; VoiceRecorderProvider now scoped locally with checkPendingRecording() deferred to chat mount. AutomaticKeepAliveClientMixin moved to inner _ChatPageView state, which correctly keeps the ChangeNotifierProvider subtree alive across tab switches. Bulk of changes are indentation/formatting reformats.
app/lib/pages/settings/developer.dart DeveloperSettingsPage converted to StatelessWidget wrapping a local ChangeNotifierProvider for DeveloperModeProvider. McpProvider kept at root (lazy) and accessed correctly via context.read from within the page subtree. DeveloperModeProvider is only consumed inside this page — scope change is safe.
app/lib/pages/settings/ai_app_generator_page.dart AiAppGeneratorPage converted to StatelessWidget with a local ChangeNotifierProvider replacing the root ChangeNotifierProxyProvider. AppProvider wiring is preserved via a manual provider.setAppProvider(context.read()) call in initState's post-frame callback — AppProvider is still accessible from root. Safe refactor.
app/lib/pages/apps/add_app.dart AddAppPage now creates a local PaymentMethodProvider() for a one-shot payment status check. The call is correctly awaited (fixing a pre-existing race condition where the old code did not await). PR description explicitly describes this as a local instance for its one-shot check. Functionally correct; the fire-and-forget getPayPalDetails() is benign.
app/lib/backend/preferences.dart Six calendar preference accessors removed (calendarId, calendarType, calendarIntegrationEnabled, showEventsWithNoParticipants, showMeetingsInMenuBar, enabledCalendarIds). calendarEnabled kept intentionally for Google Calendar OAuth integration. No remaining code references the removed accessors.
app/lib/providers/capture_provider.dart CalendarProvider import and calendarProvider field removed. Remaining changes are formatting reformats only. Clean removal with no functional regressions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Root["Root MultiProvider (app startup)"]
        direction TB
        E1[AuthProvider]
        E2[AppProvider]
        E3[CaptureProvider]
        E4[ConversationProvider]
        E5[DeviceProvider]
        E6[UserProvider]
        E7[SyncProvider]
        E8[LocaleProvider]
        E9[AnnouncementProvider]
        E10[MemoriesProvider]
        L1["ActionItemsProvider 🔵 lazy"]
        L2["GoalsProvider 🔵 lazy"]
        L3["TaskIntegrationProvider 🔵 lazy"]
        L4["IntegrationProvider 🔵 lazy"]
        L5["FolderProvider 🔵 lazy"]
        L6["McpProvider 🔵 lazy"]
        L7["PaymentMethodProvider 🔵 lazy"]
        L8["PhoneCallProvider 🔵 lazy"]
    end

    subgraph Removed["Removed from Root ❌"]
        R1[CalendarProvider]
        R2[DeveloperModeProvider]
        R3[AiAppGeneratorProvider]
        R4[VoiceRecorderProvider]
        R5[PaymentMethodProvider eager]
    end

    subgraph Screens["Screen-Scoped Providers"]
        S1["DeveloperSettingsPage\n→ DeveloperModeProvider()..initialize()"]
        S2["AiAppGeneratorPage\n→ AiAppGeneratorProvider()"]
        S3["ChatPage\n→ VoiceRecorderProvider()..checkPendingRecording()"]
        S4["AddAppPage\n→ PaymentMethodProvider() local one-shot"]
    end

    Root --> S1
    Root --> S2
    Root --> S3
    Root --> S4

    style Removed fill:#ff6b6b,color:#fff
    style L1 fill:#4ecdc4,color:#fff
    style L2 fill:#4ecdc4,color:#fff
    style L3 fill:#4ecdc4,color:#fff
    style L4 fill:#4ecdc4,color:#fff
    style L5 fill:#4ecdc4,color:#fff
    style L6 fill:#4ecdc4,color:#fff
    style L7 fill:#4ecdc4,color:#fff
    style L8 fill:#4ecdc4,color:#fff
Loading

Reviews (2): Last reviewed commit: "fix(app): restore McpProvider and Paymen..." | Re-trigger Greptile

Comment on lines +20 to +23
return ChangeNotifierProvider(
create: (_) => PaymentMethodProvider(),
child: const _PaymentsPageView(),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Provider scope breaks StripeConnectSetup and PaypalSetupPage

PaymentMethodProvider is now scoped inside PaymentsPage's widget tree. When routeToPage(context, const StripeConnectSetup()) or routeToPage(context, const PaypalSetupPage()) is called from _PaymentsPageView, those pages are pushed as new Navigator routes. A Navigator-pushed route's BuildContext is sourced from the Navigator's overlay — it is not a child of PaymentsPage's ChangeNotifierProvider. Both StripeConnectSetup.initState (context.read<PaymentMethodProvider>().getSupportedCountries()) and PaypalSetupPage.initState (context.read<PaymentMethodProvider>()) will throw ProviderNotFoundException at runtime and crash the app.

Fix: keep PaymentMethodProvider at the root MultiProvider, or wrap each navigated-to page with a ChangeNotifierProvider.value passing the existing instance before routing.

mdmohsin7 and others added 3 commits April 14, 2026 21:27
showDialog pushes CreateMcpApiKeyDialog onto the root Navigator, so
the dialog's BuildContext is a sibling of the page, not a descendant.
InheritedProvider is not captured by dialog routes, so a page-scoped
McpProvider would throw ProviderNotFoundException when the dialog
tries to read it. Keep DeveloperModeProvider scoped (only consumed
by the page itself); McpProvider moves back to root (see main.dart).
StripeConnectSetup and PaypalSetupPage are pushed via Navigator.push,
so they mount as siblings in the Navigator overlay, not children of
PaymentsPage. A page-scoped provider would throw
ProviderNotFoundException in their initState. Revert PaymentsPage to
StatefulWidget; the provider is reinstated at root (see main.dart).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both were previously scoped to their pages, but consumers live on
separate Navigator routes (a dialog and pushed setup pages) that
cannot see page-scoped providers. Restoring them at root with
lazy: true keeps cold-start cost zero — they still only construct
on first read.
@mdmohsin7
Copy link
Copy Markdown
Member Author

@greptile-apps re-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant