
Memo's user interface is a Flutter desktop application (with a mobile companion) following the "Greige" (pewter study) design language — warm neutrals, bronze accents, clear contrast, no harsh whites.
| Layer | Technology |
|---|---|
| Framework | Flutter 3.10+ (Dart) |
| State Management | Riverpod (AsyncNotifierProvider) |
| HTTP Client | Dio (request + SSE streaming) |
| Routing | GoRouter (declarative, path-based) |
| Icons | Phosphor Icons (Bold weight, MIT licensed) |
| SVG | flutter_svg (provider logos) |
| Markdown | Custom markdown renderer with code highlighting |
The color palette uses warm, muted neutrals:
| Token | Purpose | Value |
|---|---|---|
--surface |
Primary background | Warm dark grey |
--surface-2 |
Secondary surface (cards, panels) | Slightly lighter |
--ink |
Primary text | Off-white |
--ink-dim |
Secondary text | Muted warm grey |
--ink-faint |
Tertiary text, labels | Faded grey |
--line |
Borders, dividers | Subtle grey |
--bronze-bright |
Active / accent | Warm bronze/gold |
--bronze-deep |
Accent borders | Deep bronze |
--bronze-pale |
Accent backgrounds | Pale bronze wash |
Typography uses var(--body) (sans-serif, 0.88rem base) and var(--mono) (monospace, 0.84rem) font stacks.
// Example: Chat provider
final chatProvider = AsyncNotifierProvider<ChatNotifier, ChatState>(
ChatNotifier.new,
);
class ChatNotifier extends AsyncNotifier<ChatState> {
Future<void> sendMessage(String text) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final client = ref.read(apiClientProvider);
final stream = await client.postChatStream(text);
// Process SSE events, update state incrementally
return updatedState;
});
}
}
Key providers:
| Provider | Purpose |
|---|---|
chatProvider |
Active conversation state, message list, streaming |
memoryProvider |
Memory search, analytics, import/export |
providerListProvider |
External provider configurations |
modelListProvider |
Downloaded and available models |
agentProvider |
Agent execution state, permission dialogs |
orchestraProvider |
Orchestra config, role assignments |
whatsappProvider |
Connection state, messages, contacts |
calendarProvider |
Events, reminders, settings |
moodProvider |
Mood score, engine toggle |
skillListProvider |
Installed and active skills |
settingsProvider |
General app settings |
MemoApiClient// frontend/lib/core/api_client.dart
class MemoApiClient {
final Dio _dio;
static const baseUrl = 'http://localhost:8090';
// Chat
Future<Stream<SSEEvent>> postChatStream(ChatRequest req);
Future<ChatResponse> postChat(ChatRequest req);
Future<void> stopChat();
// Memory
Future<List<Memory>> searchMemory(String query, {int topK = 5});
Future<void> rememberMemory(String text);
Future<int> forgetMemory(String pattern);
Future<MemoryAnalytics> getMemoryAnalytics();
// Providers
Future<List<ProviderConfig>> getProviders();
Future<void> saveProvider(ProviderConfig config);
Future<void> deleteProvider(String name);
// ... ~90 method signatures mirroring backend endpoints
}
Dio is configured with a singleton Provider<MemoApiClient> pattern. SSE streams are parsed with dio.request() using responseType: ResponseType.stream.
| Screen | Path | Purpose |
|---|---|---|
| Chat | / |
Main chat interface with message list, input, engine strip |
| Agent | /agent |
Agent mode chat with tool-calling UI |
| Model Store | /store |
Two-panel model discovery and download |
/whatsapp |
WhatsApp chat list and messaging | |
| Calendar | /calendar |
Monthly grid, event list, add/edit |
| Settings | /settings |
12-tab configuration panel |
Each tab is a focused widget under frontend/lib/widgets/settings/tabs/:
| Tab | File | Content |
|---|---|---|
| General | general_tab.dart |
Name, language, theme, memory toggle, web search mode |
| System Prompt | system_prompt_tab.dart |
Identity prompt editor with preset selector |
| Incognito Prompt | incognito_prompt_tab.dart |
Separate prompt for incognito mode |
| Memory | memory_tab.dart |
RAG config, debug search, analytics, import/export |
| API Providers | provider_tab.dart |
Provider list, add/edit with per-model context window |
| Orchestra | orchestra_tab.dart |
Role config, chief model, quick-setup button |
| Agent Permissions | agent_permissions_tab.dart |
Tool permission policies (6-level) |
| Skills | skills_tab.dart |
Skill list, install, activate, remove |
| GPU Config | gpu_config_tab.dart |
llama.cpp settings, GPU layers, VRAM |
| Backup | backup_tab.dart |
Cloud sync setup, export, restore, wipe |
| Remote Access | remote_access_tab.dart |
ngrok, Tailscale, LAN binding |
| About | about_tab.dart |
Version, license, credits, update check |
The settings tab sidebar shows Phosphor Bold icons alongside labels. The active tab's icon lights up in accent color; inactive tabs use textDim.
A persistent footer bar showing:
┌─────────────────────────────────────────────┐
│ 🟢 llama-3.1-8b-instruct │ ◼ stop all │
│ 🟢 nomic-embed-text-v1.5 │ 🤖 OpenAI │
└─────────────────────────────────────────────┘
if (!mounted) return; before setState in async callbacksref.invalidate(provider) after mutations to trigger UI rebuildsAsyncValue.guard() wraps all async operations| Breakpoint | Behavior |
|---|---|
| > 1200px | Full sidebar (280px) + content + optional right panel |
| 860–1200px | Collapsible sidebar with toggle button |
| < 860px | Mobile layout — sidebar as overlay, full-width content |
The mobile companion app shares the same codebase and adapts the layout for smaller screens.