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/specs/2026-04-04-knowledge-view-design.md
T
bvandeusen ac4b2359a5 docs: add Knowledge View design spec for Android app
Covers four-tab navigation (Briefing/Knowledge/Chat/Projects),
two-tier pagination (50 IDs / 12-item batches), type-aware cards,
bottom sheet type picker, Projects screen with sorted project list,
ProjectEditScreen, and manifest/config checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 22:30:44 -04:00

16 KiB

Knowledge View — Android App Design Spec

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.

Goal: Replace the Library tab with a typed Knowledge view and add a dedicated Projects tab, bringing the Android app to parity with the web Knowledge view while using the existing backend /api/knowledge and /api/projects endpoints.

Architecture: Two-tier pagination — fetch IDs cheaply (50 at a time), hydrate visible items in batches of 12. Type tabs drive server-side filtering. A new Projects tab replaces the project section formerly in Library. All data access goes through the existing Riverpod/Dio/repository pattern.

Tech Stack: Flutter 3, Riverpod 3, GoRouter 17, Dio 5, existing backend REST API (/api/knowledge, /api/projects, /api/notes)


Manifest & Configuration Checklist

These must be verified before any code is written. Failures here cause silent runtime errors.

  • android/app/src/main/AndroidManifest.xml — confirm <uses-permission android:name="android.permission.INTERNET" /> is present
  • android/app/src/main/AndroidManifest.xml — confirm android:usesCleartextTraffic="true" is set on the <application> tag (required for HTTP dev server connections; if already using HTTPS only, leave as-is but document the decision)
  • pubspec.yaml — no new dependencies required for Knowledge View
  • android/app/build.gradle — no changes required for Knowledge View

File Map

New files

Path Responsibility
lib/data/models/knowledge_item.dart Unified model for all knowledge types (note, person, place, list, task)
lib/data/api/knowledge_api.dart Two API methods: fetch IDs, batch-hydrate items
lib/data/repositories/knowledge_repository.dart Thin repo wrapping KnowledgeApi
lib/providers/knowledge_provider.dart StateNotifier with two-tier pagination state
lib/screens/knowledge/knowledge_screen.dart Main Knowledge screen with type tabs, tag chips, search, infinite scroll
lib/widgets/knowledge_item_card.dart Type-aware card widget (different icon/subtitle per type)
lib/screens/projects/projects_screen.dart Project list sorted by updated_at desc
lib/screens/projects/project_edit_screen.dart Create/edit project (title, description, goal, status)

Modified files

Path Change
lib/data/models/note.dart Add noteType field (String, default 'note')
lib/data/models/project.dart Add status, goal, color, autoSummary, updatedAt fields
lib/data/api/notes_api.dart Pass note_type in create and update request bodies
lib/data/api/projects_api.dart Add sort=updated_at&order=desc to list call; add status, goal, color to create/update
lib/screens/notes/note_edit_screen.dart Accept optional noteType parameter; show type badge in app bar; pass note_type to API
lib/screens/library/project_tasks_screen.dart Add edit button in app bar pushing to ProjectEditScreen
lib/app.dart Replace Routes.library with Routes.knowledge + Routes.projects; update shell to 4 tabs
lib/core/constants.dart Add Routes.knowledge, Routes.projects; remove Routes.library

Retired files

Path Replacement
lib/screens/library/library_screen.dart KnowledgeScreen + ProjectsScreen
lib/widgets/library_item_card.dart KnowledgeItemCard

Section 1: Navigation

Four tabs replace the existing three:

Briefing  |  Knowledge  |  Chat  |  Projects

lib/core/constants.dart:

  • Remove static const library = '/library'
  • Add static const knowledge = '/knowledge'
  • projects already exists as '/projects' — no change needed
  • Add static const projectEdit = '/projects/:id/edit'

lib/app.dart_ShellState:

static const _tabs = [
  Routes.briefing,
  Routes.knowledge,
  Routes.conversations,
  Routes.projects,
];

Shell NavigationBar / NavigationRail entries:

// Bottom nav
NavigationDestination(icon: Icon(Icons.wb_sunny_outlined), selectedIcon: Icon(Icons.wb_sunny), label: 'Briefing'),
NavigationDestination(icon: Icon(Icons.menu_book_outlined), selectedIcon: Icon(Icons.menu_book), label: 'Knowledge'),
NavigationDestination(icon: Icon(Icons.chat_bubble_outline), selectedIcon: Icon(Icons.chat_bubble), label: 'Chat'),
NavigationDestination(icon: Icon(Icons.folder_outlined), selectedIcon: Icon(Icons.folder), label: 'Projects'),

_QuickCaptureBar._hintForLocation update:

String _hintForLocation(String location) {
  if (location.startsWith(Routes.knowledge)) return 'Capture a note…';
  if (location.startsWith(Routes.projects)) return 'Capture a note…';
  if (location.startsWith(Routes.conversations)) return 'Ask Fabled…';
  return 'Capture a note…';
}

GoRouter shell routes — replace Routes.library route with:

GoRoute(path: Routes.knowledge, builder: (_, _) => const KnowledgeScreen()),
GoRoute(path: Routes.projects, builder: (_, _) => const ProjectsScreen()),

Section 2: Data Models

lib/data/models/knowledge_item.dart

class KnowledgeItem {
  final int id;
  final String noteType;   // 'note' | 'person' | 'place' | 'list' | 'task'
  final String title;
  final String body;
  final List<String> tags;
  final int? projectId;
  final int? milestoneId;
  final int? parentId;
  // Task-only fields (null for non-tasks)
  final String? status;    // 'todo' | 'in_progress' | 'done' | 'cancelled'
  final String? priority;  // 'low' | 'normal' | 'high'
  final String? dueDate;
  final DateTime createdAt;
  final DateTime updatedAt;

  const KnowledgeItem({...});

  factory KnowledgeItem.fromJson(Map<String, dynamic> json) => KnowledgeItem(
    id: json['id'] as int,
    noteType: json['note_type'] as String? ?? 'note',
    title: json['title'] as String? ?? '',
    body: json['body'] as String? ?? '',
    tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ?? [],
    projectId: json['project_id'] as int?,
    milestoneId: json['milestone_id'] as int?,
    parentId: json['parent_id'] as int?,
    status: json['status'] as String?,
    priority: json['priority'] as String?,
    dueDate: json['due_date'] as String?,
    createdAt: DateTime.parse(json['created_at'] as String),
    updatedAt: DateTime.parse(json['updated_at'] as String),
  );
}

lib/data/models/note.dart — add noteType

final String noteType;  // new field

// In constructor:
required this.noteType,

// In fromJson:
noteType: json['note_type'] as String? ?? 'note',

// In toJson:
'note_type': noteType,

// In copyWith: add noteType parameter

lib/data/models/project.dart — add missing fields

final String status;       // 'active' | 'completed' | 'archived'
final String? goal;
final String? color;
final String? autoSummary;
final DateTime updatedAt;

// In fromJson:
status: json['status'] as String? ?? 'active',
goal: json['goal'] as String?,
color: json['color'] as String?,
autoSummary: json['auto_summary'] as String?,
updatedAt: DateTime.parse(json['updated_at'] as String),

Section 3: API Layer

lib/data/api/knowledge_api.dart

class KnowledgeApi {
  final Dio _dio;
  const KnowledgeApi(this._dio);

  /// Fetch page of IDs. Returns (ids, total).
  Future<(List<int>, int)> fetchIds({
    String? noteType,
    List<String> tags = const [],
    String sort = 'modified',
    String? q,
    int limit = 50,
    int offset = 0,
  }) async {
    final params = <String, dynamic>{
      'limit': limit,
      'offset': offset,
      'sort': sort,
      if (noteType != null) 'type': noteType,
      if (tags.isNotEmpty) 'tags': tags.join(','),
      if (q != null && q.isNotEmpty) 'q': q,
    };
    final response = await _dio.get('/api/knowledge/ids', queryParameters: params);
    final data = response.data as Map<String, dynamic>;
    final ids = (data['ids'] as List<dynamic>).map((e) => e as int).toList();
    final total = data['total'] as int;
    return (ids, total);
  }

  /// Batch-hydrate up to 100 IDs into full items.
  Future<List<KnowledgeItem>> fetchBatch(List<int> ids) async {
    if (ids.isEmpty) return [];
    final response = await _dio.get(
      '/api/knowledge/batch',
      queryParameters: {'ids': ids.join(',')},
    );
    final data = response.data as Map<String, dynamic>;
    return (data['items'] as List<dynamic>)
        .map((e) => KnowledgeItem.fromJson(e as Map<String, dynamic>))
        .toList();
  }

  /// Per-type counts for tab labels.
  Future<Map<String, int>> fetchCounts({List<String> tags = const []}) async {
    final params = <String, dynamic>{
      if (tags.isNotEmpty) 'tags': tags.join(','),
    };
    final response = await _dio.get('/api/knowledge/counts', queryParameters: params);
    return (response.data as Map<String, dynamic>).map(
      (k, v) => MapEntry(k, (v as num).toInt()),
    );
  }

  /// All tags for the current type filter.
  Future<List<String>> fetchTags({String? noteType}) async {
    final response = await _dio.get(
      '/api/knowledge/tags',
      queryParameters: {if (noteType != null) 'type': noteType},
    );
    return (response.data['tags'] as List<dynamic>).map((e) => e as String).toList();
  }
}

lib/data/api/projects_api.dart — add sort params + missing fields

Add sort: 'updated_at' and order: 'desc' to the getProjects query parameters. Add status, goal, color to createProject and updateProject request bodies.

lib/data/api/notes_api.dart — pass note_type

// In createNote and updateNote bodies:
if (noteType != null) 'note_type': noteType,

Section 4: Provider

lib/providers/knowledge_provider.dart

@immutable
class KnowledgeState {
  final List<int> ids;                        // all fetched IDs so far
  final Map<int, KnowledgeItem> items;        // hydrated items
  final int totalIds;
  final bool isLoadingIds;
  final bool isLoadingBatch;
  final bool hasMore;
  final String? noteType;                     // active type filter
  final List<String> activeTags;
  final String? searchQuery;
  final Map<String, int> counts;             // per-type counts

  bool get canLoadMore => hasMore && !isLoadingIds;
  // Items in ID order, only those hydrated
  List<KnowledgeItem> get orderedItems =>
      ids.where(items.containsKey).map((id) => items[id]!).toList();
}

class KnowledgeNotifier extends StateNotifier<KnowledgeState> {
  // On filter change: reset state, fetch IDs from offset 0
  void setTypeFilter(String? noteType) { ... }
  void toggleTag(String tag) { ... }
  void setSearch(String? q) { ... }  // debounce handled in UI

  // Fetch next page of IDs (50 at a time)
  Future<void> loadMoreIds() { ... }

  // Hydrate the next 12 un-hydrated IDs from the current list
  Future<void> hydrateNext() { ... }

  Future<void> refresh() { ... }  // reset + reload
}

// Provider
final knowledgeProvider =
    StateNotifierProvider<KnowledgeNotifier, KnowledgeState>(...);

Scroll trigger: KnowledgeScreen attaches a ScrollController listener. When position.pixels >= maxScrollExtent - 300:

  1. Call hydrateNext() if there are un-hydrated IDs in the list
  2. Call loadMoreIds() if all fetched IDs are hydrated and hasMore is true

Section 5: Knowledge Screen

lib/screens/knowledge/knowledge_screen.dart

Structure:

Scaffold
  AppBar
    title: 'Knowledge'
    actions: [search icon → expand TextField]
  Column
    TabBar (All | Notes | People | Places | Lists | Tasks)
      — tab labels show counts: "Notes (12)"
    AnimatedContainer (tag filter chip row, hidden when empty)
      — horizontal SingleChildScrollView of FilterChip widgets
    Expanded
      ListView.builder
        — items from knowledgeState.orderedItems
        — trailing: loading indicator when isLoadingBatch
  FAB (pen icon)
    — Tasks tab → push TaskEditScreen
    — all other tabs → showModalBottomSheet (type picker)

Pull-to-refresh: RefreshIndicator wrapping the ListView.

Empty state: Centered column with type-appropriate icon and "No [type] yet" text.

Error state: Centered text with retry button calling ref.invalidate(knowledgeProvider).

lib/widgets/knowledge_item_card.dart

ListTile-based card. Leading icon varies by type:

  • note → Icons.description_outlined
  • person → Icons.person_outlined
  • place → Icons.place_outlined
  • list → Icons.checklist_outlined
  • task → Icons.task_alt_outlined (with status colour on leading)

Subtitle shows truncated body (max 2 lines) or due date for tasks. Trailing shows tag chips (up to 2, then "+N more").

Tap → NoteDetailScreen(noteId: item.id) for knowledge types; TaskEditScreen(taskId: item.id) for tasks.


Section 6: Type Picker Bottom Sheet

Shown when FAB is tapped on any non-Tasks tab.

showModalBottomSheet(
  context: context,
  builder: (_) => Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      _TypeRow(Icons.description_outlined, 'Note', 'General note or document', 'note'),
      _TypeRow(Icons.person_outlined, 'Person', 'Contact, colleague, or reference person', 'person'),
      _TypeRow(Icons.place_outlined, 'Place', 'Location, venue, or place of interest', 'place'),
      _TypeRow(Icons.checklist_outlined, 'List', 'Checklist or structured list', 'list'),
    ],
  ),
);

Each _TypeRow on tap:

  1. Navigator.pop(context)
  2. context.push(Routes.noteNew, extra: {'noteType': selectedType})

lib/screens/notes/note_edit_screen.dart changes

  • Accept noteType from GoRouterState.extra or route parameter (default 'note')
  • Display a read-only type badge chip in the app bar subtitle
  • Pass noteType to notes_api.createNote / notes_api.updateNote

Section 7: Projects Screen

lib/screens/projects/projects_screen.dart

Scaffold
  AppBar: 'Projects'
  RefreshIndicator
    ListView.builder
      — projects from projectsProvider (sorted updated_at desc via API param)
      — each item: _ProjectCard
  FAB → push ProjectEditScreen()

_ProjectCard (inline widget):

  • Title + status badge (active = green, completed = blue, archived = grey)
  • Description (1 line, truncated)
  • Goal text if present (italic, muted)
  • Milestone progress: "3 / 5 milestones done" using milestone count from project data if available, otherwise omitted

Tap → context.push('/projects/${project.id}/tasks')ProjectTasksScreen reads projectId from the path parameter, unchanged except for the added edit button.

lib/screens/projects/project_edit_screen.dart

Fields: Title (required), Description, Goal, Status dropdown (active / completed / archived). Used for both create (no projectId) and edit (with projectId). On save: POST /api/projects or PATCH /api/projects/:id via projectsApi. On success: ref.invalidate(projectsProvider) then Navigator.pop.

lib/screens/library/project_tasks_screen.dart change

Add an edit IconButton in the AppBar.actions:

IconButton(
  icon: const Icon(Icons.edit_outlined),
  onPressed: () => context.push('/projects/$projectId/edit'),
)

Section 8: Provider & API Wiring

New providers to add to lib/providers/:

// knowledge_api_provider.dart (or in api_client_provider.dart)
final knowledgeApiProvider = Provider((ref) =>
    KnowledgeApi(ref.watch(dioProvider)));

final knowledgeRepositoryProvider = Provider((ref) =>
    KnowledgeRepository(ref.watch(knowledgeApiProvider)));

projectsProvider — add query parameters to the underlying getProjects call:

await api.getProjects(sort: 'updated_at', order: 'desc');

What This Does NOT Include

  • Voice I/O (separate spec)
  • Backend parity pass (separate spec)
  • Editing knowledge item type after creation
  • Bulk delete / multi-select in Knowledge screen
  • Knowledge graph view
  • Milestone detail screen (projects show milestone count only)