This repository has been archived on 2026-06-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FabledApp/docs/superpowers/plans/2026-03-11-app-overhaul-p2-navigation.md
T
bvandeusen 6232c7c99a feat: app overhaul — Briefing-first navigation, Library, capture queue
Plans 1-3 implemented:

Plan 1 — Foundation
- Add google_fonts ^6.2.1
- lib/core/theme.dart: slate-indigo ColorScheme (dark/light), Fraunces
  headings, GradientButton widget
- lib/providers/capture_work_queue_provider.dart: in-memory sequential
  work queue; CaptureWorkQueueNotifier drains one item at a time;
  captureResultProvider feeds snackbars to UI
- lib/app.dart: wire fabledDarkTheme/fabledLightTheme; replace blocking
  _QuickCaptureBar with queue-based implementation (progress bar, badge)

Plan 2 — Navigation
- 3-tab shell: Briefing · Library · Chat (was Notes · Tasks · Projects · Chat)
- lib/screens/library/library_screen.dart: unified notes+tasks+projects list
  with filter pills, status sub-filter for tasks, live search, FAB
- lib/widgets/library_item_card.dart: NoteLibraryCard, TaskLibraryCard
  (status cycle), ProjectLibraryCard
- lib/screens/chat/conversations_tab_screen.dart: focused replacement for
  ConversationsListScreen
- Delete 5 dead screens: notes_list, tasks_list, project_list,
  conversations_list, quick_capture

Plan 3 — Briefing
- lib/data/models/briefing_conversation.dart
- lib/data/api/briefing_api.dart: getToday, getHistory, getMessages,
  triggerSlot
- lib/providers/briefing_provider.dart: BriefingNotifier with sendReply
  (optimistic + SSE + poll, same pattern as MessagesNotifier) and refresh
- lib/widgets/chat_message_bubble.dart: extracted + redesigned shared bubble
  (ghost user bubbles, left-accent assistant bubbles)
- lib/widgets/briefing_digest_card.dart: expandable first-message card
- lib/screens/briefing/briefing_screen.dart: digest card, conversation list,
  streaming reply bar, refresh button, history overflow menu
- lib/screens/briefing/briefing_history_screen.dart: read-only past dates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 23:17:38 -04:00

37 KiB

Fabled App Overhaul — Plan 2: Navigation (Shell + Library + Cleanup)

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Prerequisite: Plan 1 (theme + capture queue) must be complete.

Goal: Replace the 4-tab shell (Notes · Tasks · Projects · Chat) with a 3-tab shell (Briefing · Library · Chat), build the unified LibraryScreen with filter pills and inline search, and delete the five screens made redundant by this change.

Architecture: app.dart's _Shell is rewritten for 3 tabs. A new LibraryScreen replaces the three separate list screens with a unified GET /api/notes + GET /api/tasks feed filtered by pill selection. A LibraryItemCard widget renders note, task, and project rows uniformly. The Briefing tab shows a placeholder screen until Plan 3 ships. Dead screens are deleted.

Tech Stack: Flutter/Dart, Riverpod, GoRouter, Dio (existing), GradientButton from Plan 1 theme.


File Map

Create:

  • lib/screens/briefing/briefing_screen.dart — placeholder (full implementation in Plan 3)
  • lib/screens/library/library_screen.dart — unified list with filter pills + search
  • lib/widgets/library_item_card.dart — note / task / project row widget

Modify:

  • lib/app.dart — rewrite _Shell for 3 tabs; add /briefing and /library routes; remove dead routes
  • lib/core/constants.dart — add briefing and library route constants
  • lib/providers/api_client_provider.dart — no changes needed (existing notes/tasks providers reused)

Delete:

  • lib/screens/notes/notes_list_screen.dart
  • lib/screens/tasks/tasks_list_screen.dart
  • lib/screens/projects/project_list_screen.dart
  • lib/screens/chat/conversations_list_screen.dart
  • lib/screens/quick_capture/quick_capture_screen.dart

Chunk 1: Constants + Placeholder Briefing Screen

Task 1: Add route constants

Files:

  • Modify: lib/core/constants.dart

  • Step 1: Read lib/core/constants.dart

Check the existing Routes class. It will have constants like notes, tasks, etc.

  • Step 2: Add new constants

Add to the Routes class:

static const briefing      = '/briefing';
static const library       = '/library';
  • Step 3: Commit
git add lib/core/constants.dart
git commit -m "feat: add briefing and library route constants"

Task 2: Briefing placeholder screen

Files:

  • Create: lib/screens/briefing/briefing_screen.dart

This is a placeholder — Plan 3 will replace it entirely. It just renders a centred message so the tab is navigable.

  • Step 1: Create lib/screens/briefing/briefing_screen.dart
import 'package:flutter/material.dart';

/// Placeholder — full implementation in Plan 3.
class BriefingScreen extends StatelessWidget {
  const BriefingScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Briefing', style: theme.textTheme.titleLarge),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.wb_sunny_outlined,
                size: 48, color: theme.colorScheme.primary),
            const SizedBox(height: 16),
            Text('Briefing coming soon',
                style: theme.textTheme.titleMedium),
            const SizedBox(height: 8),
            Text('Your daily briefing will appear here.',
                style: theme.textTheme.bodyMedium?.copyWith(
                    color: theme.colorScheme.onSurfaceVariant)),
          ],
        ),
      ),
    );
  }
}
  • Step 2: Analyze
flutter analyze lib/screens/briefing/briefing_screen.dart

Expected: no errors.

  • Step 3: Commit
git add lib/screens/briefing/briefing_screen.dart
git commit -m "feat: briefing placeholder screen"

Chunk 2: Library Screen

Task 3: LibraryItemCard widget

Files:

  • Create: lib/widgets/library_item_card.dart

A unified card that renders a note row, a task row, or a project row depending on item type. Tapping navigates to the appropriate detail screen.

  • Step 1: Read the existing data models

Check lib/data/models/note.dart, lib/data/models/task.dart, lib/data/models/project.dart to confirm field names before writing the widget.

  • Step 2: Create lib/widgets/library_item_card.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';

import '../core/constants.dart';
import '../data/models/note.dart';
import '../data/models/project.dart';
import '../data/models/task.dart';
import '../providers/tasks_provider.dart';

// ── Note card ────────────────────────────────────────────────────────────────

class NoteLibraryCard extends StatelessWidget {
  final Note note;
  const NoteLibraryCard({super.key, required this.note});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      child: InkWell(
        onTap: () => context.push(
            Routes.noteDetail.replaceFirst(':id', '${note.id}')),
        borderRadius: BorderRadius.circular(14),
        child: Padding(
          padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Icon(Icons.article_outlined,
                      size: 15,
                      color: theme.colorScheme.onSurfaceVariant),
                  const SizedBox(width: 6),
                  Expanded(
                    child: Text(
                      note.title.isNotEmpty ? note.title : 'Untitled',
                      style: theme.textTheme.titleSmall,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                  Text(
                    _relativeTime(note.updatedAt),
                    style: theme.textTheme.labelSmall?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant),
                  ),
                ],
              ),
              if (note.body.isNotEmpty) ...[
                const SizedBox(height: 4),
                Text(
                  note.body.replaceAll('\n', ' '),
                  style: theme.textTheme.bodySmall?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ],
              if (note.tags.isNotEmpty) ...[
                const SizedBox(height: 6),
                Wrap(
                  spacing: 4,
                  runSpacing: 2,
                  children: note.tags
                      .take(4)
                      .map((t) => Chip(
                            label: Text(t),
                            materialTapTargetSize:
                                MaterialTapTargetSize.shrinkWrap,
                            padding: EdgeInsets.zero,
                            visualDensity: VisualDensity.compact,
                          ))
                      .toList(),
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

// ── Task card ────────────────────────────────────────────────────────────────

class TaskLibraryCard extends ConsumerWidget {
  final Task task;
  const TaskLibraryCard({super.key, required this.task});

  Color _priorityColor(BuildContext context, String? priority) {
    final cs = Theme.of(context).colorScheme;
    return switch (priority) {
      'high'   => const Color(0xFFEF4444),
      'medium' => const Color(0xFFF59E0B),
      _        => cs.onSurfaceVariant,
    };
  }

  IconData _statusIcon(String status) => switch (status) {
        'done'        => Icons.check_circle,
        'in_progress' => Icons.timelapse,
        _             => Icons.radio_button_unchecked,
      };

  Color _statusColor(BuildContext context, String status) {
    final cs = Theme.of(context).colorScheme;
    return switch (status) {
      'done'        => const Color(0xFF22C55E),
      'in_progress' => cs.primary,
      _             => cs.onSurfaceVariant,
    };
  }

  String _nextStatus(String current) => switch (current) {
        'todo'        => 'in_progress',
        'in_progress' => 'done',
        _             => 'todo',
      };

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final theme = Theme.of(context);
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      child: InkWell(
        onTap: () => context.push(
            Routes.taskEdit.replaceFirst(':id', '${task.id}')),
        borderRadius: BorderRadius.circular(14),
        child: Padding(
          padding: const EdgeInsets.fromLTRB(8, 10, 14, 10),
          child: Row(
            children: [
              // Status cycle button
              IconButton(
                icon: Icon(
                  _statusIcon(task.status),
                  color: _statusColor(context, task.status),
                ),
                onPressed: () => ref
                    .read(tasksProvider.notifier)
                    .updateStatus(task.id, _nextStatus(task.status)),
                tooltip: 'Cycle status',
                visualDensity: VisualDensity.compact,
              ),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      task.title.isNotEmpty ? task.title : 'Untitled',
                      style: theme.textTheme.titleSmall?.copyWith(
                        decoration: task.status == 'done'
                            ? TextDecoration.lineThrough
                            : null,
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    if (task.dueDate != null) ...[
                      const SizedBox(height: 2),
                      Text(
                        'Due ${DateFormat.MMMd().format(task.dueDate!)}',
                        style: theme.textTheme.labelSmall?.copyWith(
                          color: task.dueDate!.isBefore(DateTime.now()) &&
                                  task.status != 'done'
                              ? const Color(0xFFEF4444)
                              : theme.colorScheme.onSurfaceVariant,
                        ),
                      ),
                    ],
                  ],
                ),
              ),
              if (task.priority != null && task.priority != 'none')
                Container(
                  width: 8,
                  height: 8,
                  decoration: BoxDecoration(
                    color: _priorityColor(context, task.priority),
                    shape: BoxShape.circle,
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

// ── Project card ─────────────────────────────────────────────────────────────

class ProjectLibraryCard extends StatelessWidget {
  final Project project;
  const ProjectLibraryCard({super.key, required this.project});

  Color _parseColor(String? hex) {
    if (hex == null || hex.isEmpty) return const Color(0xFF6366F1);
    try {
      return Color(int.parse(hex.replaceFirst('#', '0xFF')));
    } catch (_) {
      return const Color(0xFF6366F1);
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final color = _parseColor(project.color);
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: () {}, // Projects have no detail screen in this app
        borderRadius: BorderRadius.circular(14),
        child: Row(
          children: [
            // Colour strip
            Container(width: 6, height: 64, color: color),
            const SizedBox(width: 12),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.symmetric(vertical: 12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      project.title,
                      style: theme.textTheme.titleSmall,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    if (project.description?.isNotEmpty == true) ...[
                      const SizedBox(height: 2),
                      Text(
                        project.description!,
                        style: theme.textTheme.bodySmall?.copyWith(
                            color: theme.colorScheme.onSurfaceVariant),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ],
                ),
              ),
            ),
            const SizedBox(width: 12),
          ],
        ),
      ),
    );
  }
}

// ── Helpers ───────────────────────────────────────────────────────────────────

String _relativeTime(DateTime? dt) {
  if (dt == null) return '';
  final diff = DateTime.now().difference(dt);
  if (diff.inMinutes < 1) return 'just now';
  if (diff.inHours < 1) return '${diff.inMinutes}m ago';
  if (diff.inDays < 1) return '${diff.inHours}h ago';
  if (diff.inDays < 7) return '${diff.inDays}d ago';
  return DateFormat.MMMd().format(dt);
}

Note: Routes.noteDetail and Routes.taskEdit use path patterns like /notes/:id — check lib/core/constants.dart for exact values and adjust replaceFirst calls accordingly.

Also note: tasksProvider.notifier.updateStatus() — check lib/providers/tasks_provider.dart to verify this method exists. If not, add it (it calls PATCH /api/tasks/:id with {status}).

Also: intl package may need adding to pubspec.yaml — check if it's already a transitive dependency via flutter pub deps. If not, add intl: ^0.19.0.

  • Step 3: Analyze
flutter analyze lib/widgets/library_item_card.dart

Fix any type errors (common: nullable field access, missing fromJson fields on models).

  • Step 4: Commit
git add lib/widgets/library_item_card.dart
git commit -m "feat: LibraryItemCard — note, task, and project row widgets"

Task 4: LibraryScreen

Files:

  • Create: lib/screens/library/library_screen.dart

  • Step 1: Create lib/screens/library/library_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

import '../../core/constants.dart';
import '../../data/models/note.dart';
import '../../data/models/task.dart';
import '../../providers/notes_provider.dart';
import '../../providers/projects_provider.dart';
import '../../providers/tasks_provider.dart';
import '../../widgets/library_item_card.dart';

enum _LibraryFilter { all, notes, tasks, projects }
enum _TaskStatusFilter { all, todo, inProgress, done }

class LibraryScreen extends ConsumerStatefulWidget {
  const LibraryScreen({super.key});

  @override
  ConsumerState<LibraryScreen> createState() => _LibraryScreenState();
}

class _LibraryScreenState extends ConsumerState<LibraryScreen> {
  _LibraryFilter _filter = _LibraryFilter.all;
  _TaskStatusFilter _taskStatus = _TaskStatusFilter.all;
  bool _searchActive = false;
  String _searchQuery = '';
  final _searchController = TextEditingController();

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  bool _matchesSearch(String text) {
    if (_searchQuery.isEmpty) return true;
    return text.toLowerCase().contains(_searchQuery.toLowerCase());
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final notesAsync = ref.watch(notesProvider);
    final tasksAsync = ref.watch(tasksProvider);
    final projectsAsync = ref.watch(projectsProvider);

    return Scaffold(
      appBar: AppBar(
        title: _searchActive
            ? TextField(
                controller: _searchController,
                autofocus: true,
                decoration: const InputDecoration(
                  hintText: 'Search…',
                  border: InputBorder.none,
                  isDense: true,
                ),
                onChanged: (q) => setState(() => _searchQuery = q),
              )
            : Text('Library', style: theme.textTheme.titleLarge),
        actions: [
          IconButton(
            icon: Icon(_searchActive ? Icons.close : Icons.search),
            onPressed: () => setState(() {
              _searchActive = !_searchActive;
              if (!_searchActive) {
                _searchQuery = '';
                _searchController.clear();
              }
            }),
          ),
        ],
      ),
      body: Column(
        children: [
          // ── Filter pills ──────────────────────────────────────────────────
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
            child: Row(
              children: _LibraryFilter.values.map((f) {
                final label = switch (f) {
                  _LibraryFilter.all      => 'All',
                  _LibraryFilter.notes    => 'Notes',
                  _LibraryFilter.tasks    => 'Tasks',
                  _LibraryFilter.projects => 'Projects',
                };
                final selected = _filter == f;
                return Padding(
                  padding: const EdgeInsets.only(right: 8),
                  child: FilterChip(
                    label: Text(label),
                    selected: selected,
                    onSelected: (_) => setState(() {
                      _filter = f;
                      _taskStatus = _TaskStatusFilter.all;
                    }),
                    selectedColor:
                        theme.colorScheme.primary.withValues(alpha: 0.18),
                    checkmarkColor: theme.colorScheme.primary,
                  ),
                );
              }).toList(),
            ),
          ),

          // ── Task status sub-filter (Tasks pill only) ───────────────────
          if (_filter == _LibraryFilter.tasks)
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              padding: const EdgeInsets.fromLTRB(12, 0, 12, 4),
              child: Row(
                children: _TaskStatusFilter.values.map((s) {
                  final label = switch (s) {
                    _TaskStatusFilter.all        => 'All',
                    _TaskStatusFilter.todo       => 'To Do',
                    _TaskStatusFilter.inProgress => 'In Progress',
                    _TaskStatusFilter.done       => 'Done',
                  };
                  return Padding(
                    padding: const EdgeInsets.only(right: 8),
                    child: ChoiceChip(
                      label: Text(label),
                      selected: _taskStatus == s,
                      onSelected: (_) => setState(() => _taskStatus = s),
                      selectedColor:
                          theme.colorScheme.secondary.withValues(alpha: 0.15),
                    ),
                  );
                }).toList(),
              ),
            ),

          const Divider(height: 1),

          // ── Content ───────────────────────────────────────────────────────
          Expanded(
            child: switch (_filter) {
              _LibraryFilter.notes    => _buildNotesList(notesAsync),
              _LibraryFilter.tasks    => _buildTasksList(tasksAsync),
              _LibraryFilter.projects => _buildProjectsList(projectsAsync),
              _LibraryFilter.all      => _buildAllList(notesAsync, tasksAsync),
            },
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showCreateSheet(context),
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildNotesList(AsyncValue<List<Note>> notesAsync) {
    return notesAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('Error: $e')),
      data: (notes) {
        final filtered = notes
            .where((n) => _matchesSearch(n.title) || _matchesSearch(n.body))
            .toList();
        if (filtered.isEmpty) {
          return const Center(child: Text('No notes found'));
        }
        return RefreshIndicator(
          onRefresh: () async => ref.invalidate(notesProvider),
          child: ListView.builder(
            itemCount: filtered.length,
            itemBuilder: (_, i) => NoteLibraryCard(note: filtered[i]),
          ),
        );
      },
    );
  }

  Widget _buildTasksList(AsyncValue<List<Task>> tasksAsync) {
    return tasksAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('Error: $e')),
      data: (tasks) {
        var filtered = tasks
            .where((t) => _matchesSearch(t.title))
            .toList();
        if (_taskStatus != _TaskStatusFilter.all) {
          final status = switch (_taskStatus) {
            _TaskStatusFilter.todo       => 'todo',
            _TaskStatusFilter.inProgress => 'in_progress',
            _TaskStatusFilter.done       => 'done',
            _                            => '',
          };
          filtered = filtered.where((t) => t.status == status).toList();
        }
        if (filtered.isEmpty) {
          return const Center(child: Text('No tasks found'));
        }
        return RefreshIndicator(
          onRefresh: () async => ref.invalidate(tasksProvider),
          child: ListView.builder(
            itemCount: filtered.length,
            itemBuilder: (_, i) => TaskLibraryCard(task: filtered[i]),
          ),
        );
      },
    );
  }

  Widget _buildProjectsList(AsyncValue projects) {
    return projects.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('Error: $e')),
      data: (data) {
        final list = data as List;
        if (list.isEmpty) {
          return const Center(child: Text('No projects'));
        }
        return RefreshIndicator(
          onRefresh: () async => ref.invalidate(projectsProvider),
          child: ListView.builder(
            itemCount: list.length,
            itemBuilder: (_, i) => ProjectLibraryCard(project: list[i]),
          ),
        );
      },
    );
  }

  Widget _buildAllList(
    AsyncValue<List<Note>> notesAsync,
    AsyncValue<List<Task>> tasksAsync,
  ) {
    final notes = notesAsync.valueOrNull ?? [];
    final tasks = tasksAsync.valueOrNull ?? [];

    // Merge and sort by updatedAt desc
    final items = <(DateTime, Widget)>[];
    for (final n in notes) {
      if (_matchesSearch(n.title) || _matchesSearch(n.body)) {
        items.add((n.updatedAt ?? DateTime(0), NoteLibraryCard(note: n)));
      }
    }
    for (final t in tasks) {
      if (_matchesSearch(t.title)) {
        items.add((t.updatedAt ?? DateTime(0), TaskLibraryCard(task: t)));
      }
    }
    items.sort((a, b) => b.$1.compareTo(a.$1));

    if (notesAsync.isLoading || tasksAsync.isLoading) {
      return const Center(child: CircularProgressIndicator());
    }
    if (items.isEmpty) {
      return const Center(child: Text('Nothing here yet'));
    }
    return RefreshIndicator(
      onRefresh: () async {
        ref.invalidate(notesProvider);
        ref.invalidate(tasksProvider);
      },
      child: ListView.builder(
        itemCount: items.length,
        itemBuilder: (_, i) => items[i].$2,
      ),
    );
  }

  void _showCreateSheet(BuildContext context) {
    showModalBottomSheet<void>(
      context: context,
      builder: (_) => SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.article_outlined),
              title: const Text('New note'),
              onTap: () {
                Navigator.pop(context);
                context.push(Routes.noteNew);
              },
            ),
            ListTile(
              leading: const Icon(Icons.check_box_outlined),
              title: const Text('New task'),
              onTap: () {
                Navigator.pop(context);
                context.push(Routes.taskNew);
              },
            ),
          ],
        ),
      ),
    );
  }
}
  • Step 2: Check model field names

Read lib/data/models/note.dart and lib/data/models/task.dart. Verify:

  • Note has: id, title, body, tags (List), updatedAt (DateTime?)
  • Task has: id, title, status, priority, dueDate (DateTime?), updatedAt (DateTime?)
  • Project has: id, title, description, color

Adjust field names in LibraryScreen and LibraryItemCard if they differ.

  • Step 3: Verify tasksProvider.notifier has updateStatus()

Check lib/providers/tasks_provider.dart. If updateStatus(int id, String status) doesn't exist, add it:

Future<void> updateStatus(int id, String status) async {
  await ref.read(tasksRepositoryProvider).updateStatus(id, status);
  ref.invalidateSelf();
}

And add to lib/data/repositories/tasks_repository.dart:

Future<void> updateStatus(int id, String status) async {
  await _api.updateStatus(id, status);
}

And add to lib/data/api/tasks_api.dart:

Future<void> updateStatus(int id, String status) async {
  try {
    await _dio.patch('/api/tasks/$id/status', data: {'status': status});
  } on DioException catch (e) {
    throw dioToApp(e);
  }
}
  • Step 4: Analyze
flutter analyze lib/screens/library/ lib/widgets/library_item_card.dart

Fix any errors.

  • Step 5: Commit
git add lib/screens/library/ lib/widgets/library_item_card.dart
git commit -m "feat: LibraryScreen — unified notes/tasks/projects with filter pills and search"

Chunk 3: Shell Rewrite + Dead Code Cleanup

Task 5: Rewrite the shell and routes

Files:

  • Modify: lib/app.dart_Shell class, routerProvider routes list

  • Step 1: Update imports in lib/app.dart

Add:

import 'screens/briefing/briefing_screen.dart';
import 'screens/library/library_screen.dart';

Remove imports for deleted screens (they will be deleted next):

// Remove these:
import 'screens/notes/notes_list_screen.dart';
import 'screens/tasks/tasks_list_screen.dart';
import 'screens/projects/project_list_screen.dart';
import 'screens/chat/conversations_list_screen.dart';
  • Step 2: Update the shell tab list

Find static const _tabs = [Routes.notes, Routes.tasks, Routes.projects, Routes.conversations];

Replace with:

static const _tabs = [Routes.briefing, Routes.library, Routes.conversations];
  • Step 3: Update NavigationBar destinations

Replace the 4-destination NavigationBar with:

bottomNavigationBar: NavigationBar(
  selectedIndex: index,
  onDestinationSelected: (i) => context.go(_tabs[i]),
  destinations: const [
    NavigationDestination(
      icon: Icon(Icons.wb_sunny_outlined),
      selectedIcon: Icon(Icons.wb_sunny),
      label: 'Briefing',
    ),
    NavigationDestination(
      icon: Icon(Icons.library_books_outlined),
      selectedIcon: Icon(Icons.library_books),
      label: 'Library',
    ),
    NavigationDestination(
      icon: Icon(Icons.chat_bubble_outline),
      selectedIcon: Icon(Icons.chat_bubble),
      label: 'Chat',
    ),
  ],
),
  • Step 4: Update NavigationRail destinations (wide layout)

Replace the 4-destination NavigationRail with:

NavigationRail(
  selectedIndex: index,
  onDestinationSelected: (i) => context.go(_tabs[i]),
  labelType: NavigationRailLabelType.all,
  destinations: const [
    NavigationRailDestination(
      icon: Icon(Icons.wb_sunny_outlined),
      selectedIcon: Icon(Icons.wb_sunny),
      label: Text('Briefing'),
    ),
    NavigationRailDestination(
      icon: Icon(Icons.library_books_outlined),
      selectedIcon: Icon(Icons.library_books),
      label: Text('Library'),
    ),
    NavigationRailDestination(
      icon: Icon(Icons.chat_bubble_outline),
      selectedIcon: Icon(Icons.chat_bubble),
      label: Text('Chat'),
    ),
  ],
),
  • Step 5: Update the ShellRoute in routerProvider

Find the ShellRoute with its child GoRoutes. Replace:

ShellRoute(
  builder: (context, state, child) => _Shell(child: child),
  routes: [
    GoRoute(path: Routes.briefing,       builder: (_, _) => const BriefingScreen()),
    GoRoute(path: Routes.library,        builder: (_, _) => const LibraryScreen()),
    GoRoute(path: Routes.conversations,  builder: (_, _) => const ConversationsInlineScreen()),
  ],
),

For the Chat tab: the existing ConversationsListScreen is being deleted. Replace it with an inline conversations list built directly in the ShellRoute, or create a minimal lib/screens/chat/conversations_tab_screen.dart that replaces it. The simplest approach: move the conversations list content inline.

Create lib/screens/chat/conversations_tab_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

import '../../core/constants.dart';
import '../../providers/chat_provider.dart';

class ConversationsTabScreen extends ConsumerWidget {
  const ConversationsTabScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final theme = Theme.of(context);
    final convsAsync = ref.watch(conversationsProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Chat', style: theme.textTheme.titleLarge),
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            tooltip: 'New conversation',
            onPressed: () async {
              final conv = await ref
                  .read(conversationsProvider.notifier)
                  .create('New conversation');
              if (context.mounted) {
                context.push(Routes.chat.replaceFirst(':id', '${conv.id}'));
              }
            },
          ),
        ],
      ),
      body: convsAsync.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (e, _) => Center(child: Text('Error: $e')),
        data: (convs) {
          if (convs.isEmpty) {
            return Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(Icons.chat_bubble_outline,
                      size: 48,
                      color: theme.colorScheme.onSurfaceVariant),
                  const SizedBox(height: 16),
                  Text('No conversations yet',
                      style: theme.textTheme.titleMedium),
                  const SizedBox(height: 8),
                  FilledButton.icon(
                    icon: const Icon(Icons.add),
                    label: const Text('Start a conversation'),
                    onPressed: () async {
                      final conv = await ref
                          .read(conversationsProvider.notifier)
                          .create('New conversation');
                      if (context.mounted) {
                        context.push(
                            Routes.chat.replaceFirst(':id', '${conv.id}'));
                      }
                    },
                  ),
                ],
              ),
            );
          }
          return RefreshIndicator(
            onRefresh: () async => ref.invalidate(conversationsProvider),
            child: ListView.builder(
              itemCount: convs.length,
              itemBuilder: (ctx, i) {
                final c = convs[i];
                return ListTile(
                  leading: const Icon(Icons.chat_bubble_outline),
                  title: Text(c.title, maxLines: 1, overflow: TextOverflow.ellipsis),
                  subtitle: c.updatedAt != null
                      ? Text(
                          _relativeTime(c.updatedAt!),
                          style: theme.textTheme.labelSmall,
                        )
                      : null,
                  trailing: IconButton(
                    icon: const Icon(Icons.delete_outline),
                    onPressed: () => _confirmDelete(context, ref, c.id, c.title),
                  ),
                  onTap: () => ctx.push(
                      Routes.chat.replaceFirst(':id', '${c.id}')),
                );
              },
            ),
          );
        },
      ),
    );
  }

  Future<void> _confirmDelete(
      BuildContext context, WidgetRef ref, int id, String title) async {
    final ok = await showDialog<bool>(
      context: context,
      builder: (_) => AlertDialog(
        title: const Text('Delete conversation?'),
        content: Text('"$title" will be permanently deleted.'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
          FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
        ],
      ),
    );
    if (ok == true) {
      await ref.read(conversationsProvider.notifier).delete(id);
    }
  }
}

String _relativeTime(DateTime dt) {
  final diff = DateTime.now().difference(dt);
  if (diff.inMinutes < 1) return 'just now';
  if (diff.inHours < 1) return '${diff.inMinutes}m ago';
  if (diff.inDays < 1) return '${diff.inHours}h ago';
  if (diff.inDays < 7) return '${diff.inDays}d ago';
  return '${dt.day}/${dt.month}/${dt.year}';
}

Then in the ShellRoute, use ConversationsTabScreen:

import 'screens/chat/conversations_tab_screen.dart';
// ...
GoRoute(path: Routes.conversations, builder: (_, _) => const ConversationsTabScreen()),
  • Step 6: Update initial location

In GoRouter(initialLocation: ...), change:

initialLocation: Routes.splash,

(This stays — the splash screen handles auth redirect. After auth, redirect to briefing. Update the redirect logic to send authenticated users to Routes.briefing instead of falling through to Routes.notes.)

In the redirect callback, ensure unauthenticated users still go to login, and authenticated users landing on / or /notes//tasks//projects are redirected to /briefing. The simplest: no change needed — GoRouter will use the shell's first tab (Routes.briefing) naturally as the initial shell route.

  • Step 7: Analyze
flutter analyze lib/app.dart lib/screens/

Fix any errors.

  • Step 8: Commit
git add lib/app.dart lib/screens/chat/conversations_tab_screen.dart lib/core/constants.dart
git commit -m "feat: 3-tab shell — Briefing, Library, Chat"

Task 6: Delete dead screens

Files:

  • Delete: 5 screen files

  • Step 1: Delete the dead files

cd /home/bvandeusen/Nextcloud/Projects/fabled_app
git rm lib/screens/notes/notes_list_screen.dart
git rm lib/screens/tasks/tasks_list_screen.dart
git rm lib/screens/projects/project_list_screen.dart
git rm lib/screens/chat/conversations_list_screen.dart
git rm lib/screens/quick_capture/quick_capture_screen.dart
  • Step 2: Verify no remaining imports
grep -r "notes_list_screen\|tasks_list_screen\|project_list_screen\|conversations_list_screen\|quick_capture_screen" lib/

Expected: no output.

  • Step 3: Analyze full project
flutter analyze

Expected: zero errors.

  • Step 4: Run and verify
flutter run --debug

Verify:

  • App opens to Briefing tab (placeholder screen)

  • Library tab shows unified list with filter pills

  • Tasks filter → status sub-filter appears

  • Tapping a note navigates to NoteDetailScreen

  • Chat tab shows conversations list

  • FAB on Library → bottom sheet with "New note" / "New task"

  • Step 5: Commit

git commit -m "chore: delete dead list screens replaced by LibraryScreen"

Verification Checklist

  • flutter analyze — zero errors
  • App opens to Briefing tab
  • Library → All: notes and tasks interleaved, sorted by date
  • Library → Notes: only notes
  • Library → Tasks: tasks + status sub-filter works
  • Library → Projects: project cards with colour strip
  • Library search: typing filters results live
  • Task status cycle: tapping checkbox icon on a task card cycles status
  • FAB → bottom sheet → "New note" / "New task" navigates correctly
  • Chat tab: conversations list, new conversation creates and navigates to ChatScreen
  • No references to deleted files remain