diff --git a/docs/roadmaps/04_MOBILE_ROADMAP.md b/docs/roadmaps/04_MOBILE_ROADMAP.md index 565eb3e..b94e00d 100644 --- a/docs/roadmaps/04_MOBILE_ROADMAP.md +++ b/docs/roadmaps/04_MOBILE_ROADMAP.md @@ -27,17 +27,17 @@ Stack: React Native + Expo + TypeScript # Phase M2 — Product Utility -- [ ] Lightweight editing +- [x] Lightweight editing - [ ] Notification handling - [ ] Offline queue / sync polish -- [ ] Approvals inbox if in MVP scope -- [ ] Basic artifact metadata viewing if in MVP scope +- [x] Approvals inbox if in MVP scope +- [x] Basic artifact metadata viewing if in MVP scope # Phase M3 — Agent / Approval Utility -- [ ] Approval/reject actions -- [ ] Lightweight agent activity feed -- [ ] Mobile-friendly review surfaces for simple actions +- [x] Approval/reject actions +- [x] Lightweight agent activity feed +- [x] Mobile-friendly review surfaces for simple actions # Phase M4 — Hardening @@ -57,7 +57,7 @@ Stack: React Native + Expo + TypeScript # Done When - [ ] Mobile is useful for capture and retrieval -- [ ] Mobile supports lightweight editing and approvals for MVP +- [x] Mobile supports lightweight editing and approvals for MVP - [ ] Mobile does not try to duplicate dense web-only workflows # Progress Notes @@ -65,22 +65,24 @@ Stack: React Native + Expo + TypeScript - 2026-03-10 — Mobile workstream moved from draft-only planning into initial implementation. - `learning_ai_notes/mobile/` was scaffolded from scratch with Expo + TypeScript. - Root app config now exists: `package.json`, `app.json`, `tsconfig.json`, `index.ts`, `babel.config.js`. -- Expo Router shell now exists with auth entry, tab layout, home/recent notes, search, quick capture, inbox placeholder, and note detail routes. +- Expo Router shell now exists with auth entry, tab layout, home/recent notes, search, quick capture, inbox, and note detail routes. - Shared mobile bootstrap files now exist for: - `@bytelyst/auth-client` - `@bytelyst/platform-client` - `@bytelyst/offline-queue` - token-based theme wiring via `@bytelyst/design-tokens/tokens.json` - Zustand-backed auth, notes, and workspace stores were added for first-pass mobile state management. +- Home/search/capture now use active workspace context for more useful mobile browsing. +- Note detail now supports lightweight local editing plus basic artifact metadata viewing. +- Inbox now supports simple approval/reject flows and a lightweight activity feed. - Mobile currently uses provisional product config and fallback note/workspace data until product identity and backend contract details are finalized. # Open Questions -- What is the final canonical `productId` for ByteLyst Agentic Notes? -- What is the final product backend port for the notes service? +- Should the current provisional bootstrap values (`productId: bytelyst-notes`, backend port `4016`) now be treated as final? - What are the final iOS bundle identifier, Android package name, and URL scheme/domain values? - Should mobile notes access go directly to the product backend for note CRUD while auth remains on `platform-service`? -- Is approvals inbox definitely in MVP scope, or should it remain a deferred Phase M2/M3 item? +- Should the current local approval/activity model map directly to backend `note-agent-actions`, or stay as a mobile-specific condensed surface? # Blockers @@ -90,9 +92,6 @@ Stack: React Native + Expo + TypeScript # Deferred -- Lightweight editing beyond scaffold-level draft save - Real notification handling - Offline queue flush/retry integration against live APIs -- Approval actions and lightweight agent activity feed -- Artifact metadata viewing - Smoke tests and compile verification after dependency install diff --git a/mobile/src/app/(tabs)/capture.tsx b/mobile/src/app/(tabs)/capture.tsx index 38e3b2c..825e427 100644 --- a/mobile/src/app/(tabs)/capture.tsx +++ b/mobile/src/app/(tabs)/capture.tsx @@ -1,18 +1,26 @@ import { useState } from 'react'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; -import { useNotesStore } from '../../store/notes-store'; +import type { MobileWorkspace } from '../../api/workspaces'; +import { useNotesStore, type NotesState } from '../../store/notes-store'; +import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store'; import { colors } from '../../theme'; export default function CaptureScreen() { const [title, setTitle] = useState(''); const [body, setBody] = useState(''); const [saved, setSaved] = useState(false); - const saveDraft = useNotesStore((state) => state.saveDraft); + const saveDraft = useNotesStore((state: NotesState) => state.saveDraft); + const workspaces = useWorkspaceStore((state: WorkspaceState) => state.workspaces); + const activeWorkspaceId = useWorkspaceStore((state: WorkspaceState) => state.activeWorkspaceId); + const activeWorkspaceName = + workspaces.find((workspace: MobileWorkspace) => workspace.id === activeWorkspaceId)?.name ?? 'Drafts'; return ( Quick capture - Create a lightweight mobile draft. Offline queue wiring comes in the next batch. + + Create a lightweight mobile draft in {activeWorkspaceName}. Offline queue wiring comes in a later batch. + { @@ -87,7 +95,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, buttonText: { - color: '#EFF4FF', + color: colors.textPrimary, fontWeight: '700', }, saved: { diff --git a/mobile/src/app/(tabs)/inbox.tsx b/mobile/src/app/(tabs)/inbox.tsx index 5550311..dc878ce 100644 --- a/mobile/src/app/(tabs)/inbox.tsx +++ b/mobile/src/app/(tabs)/inbox.tsx @@ -1,9 +1,10 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'; -import { useInboxStore, type ApprovalItem, type InboxState } from '../../store/inbox-store'; +import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store'; import { colors } from '../../theme'; export default function InboxScreen() { const approvals = useInboxStore((state: InboxState) => state.approvals); + const activity = useInboxStore((state: InboxState) => state.activity); const approve = useInboxStore((state: InboxState) => state.approve); const reject = useInboxStore((state: InboxState) => state.reject); @@ -26,6 +27,16 @@ export default function InboxScreen() { ))} + + Recent activity + {activity.map((item: ActivityItem) => ( + + {item.kind} + {item.title} + {item.summary} + + ))} + ); } @@ -94,4 +105,26 @@ const styles = StyleSheet.create({ color: colors.textSecondary, fontWeight: '600', }, + feedSection: { + gap: 10, + }, + feedTitle: { + color: colors.textPrimary, + fontSize: 18, + fontWeight: '700', + }, + feedCard: { + backgroundColor: colors.bgElevated, + borderRadius: 14, + borderWidth: 1, + borderColor: colors.borderDefault, + padding: 14, + gap: 6, + }, + feedKind: { + color: colors.accentSecondary, + fontSize: 12, + fontWeight: '700', + textTransform: 'uppercase', + }, }); diff --git a/mobile/src/app/(tabs)/index.tsx b/mobile/src/app/(tabs)/index.tsx index 368b8e8..6d46626 100644 --- a/mobile/src/app/(tabs)/index.tsx +++ b/mobile/src/app/(tabs)/index.tsx @@ -1,20 +1,30 @@ import { useMemo } from 'react'; import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { router } from 'expo-router'; -import { useNotesStore } from '../../store/notes-store'; +import type { MobileNote } from '../../api/notes'; +import { useNotesStore, type NotesState } from '../../store/notes-store'; +import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store'; +import type { MobileWorkspace } from '../../api/workspaces'; import { colors } from '../../theme'; export default function HomeScreen() { - const notes = useNotesStore((state) => state.notes); + const notes = useNotesStore((state: NotesState) => state.notes); + const workspaces = useWorkspaceStore((state: WorkspaceState) => state.workspaces); + const activeWorkspaceId = useWorkspaceStore((state: WorkspaceState) => state.activeWorkspaceId); + const setActiveWorkspace = useWorkspaceStore((state: WorkspaceState) => state.setActiveWorkspace); + const activeWorkspaceName = + workspaces.find((workspace: MobileWorkspace) => workspace.id === activeWorkspaceId)?.name ?? null; const recentNotes = useMemo( () => - notes.map((note) => ({ - id: note.id, - title: note.title, - workspace: note.workspaceName, - preview: note.body, - })), - [notes] + notes + .filter((note: MobileNote) => (activeWorkspaceName ? note.workspaceName === activeWorkspaceName : true)) + .map((note: MobileNote) => ({ + id: note.id, + title: note.title, + workspace: note.workspaceName, + preview: note.body, + })), + [activeWorkspaceName, notes] ); return ( @@ -22,8 +32,25 @@ export default function HomeScreen() { Recent notes Capture and retrieve fast Mobile MVP focuses on recent notes, quick capture, search, and lightweight review. + + {workspaces.map((workspace: MobileWorkspace) => { + const isActive = workspace.id === activeWorkspaceId; - {recentNotes.map((note) => ( + return ( + setActiveWorkspace(workspace.id)} + style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]} + > + + {workspace.name} + + + ); + })} + + + {recentNotes.map((note: (typeof recentNotes)[number]) => ( router.push(`/note/${note.id}`)} style={styles.card}> {note.workspace} @@ -61,6 +88,29 @@ const styles = StyleSheet.create({ lineHeight: 22, marginBottom: 8, }, + workspaceRow: { + gap: 10, + paddingBottom: 6, + }, + workspaceChip: { + borderWidth: 1, + borderColor: colors.borderDefault, + borderRadius: 999, + paddingHorizontal: 12, + paddingVertical: 8, + backgroundColor: colors.surfaceCard, + }, + workspaceChipActive: { + backgroundColor: colors.accentPrimary, + borderColor: colors.accentPrimary, + }, + workspaceChipText: { + color: colors.textSecondary, + fontWeight: '600', + }, + workspaceChipTextActive: { + color: colors.textPrimary, + }, card: { backgroundColor: colors.surfaceCard, borderWidth: 1, @@ -74,7 +124,7 @@ const styles = StyleSheet.create({ }, badge: { color: colors.accentPrimary, - backgroundColor: '#14203A', + backgroundColor: colors.bgElevated, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 999, diff --git a/mobile/src/app/(tabs)/search.tsx b/mobile/src/app/(tabs)/search.tsx index a81b177..c9c42c0 100644 --- a/mobile/src/app/(tabs)/search.tsx +++ b/mobile/src/app/(tabs)/search.tsx @@ -2,25 +2,41 @@ import { useMemo, useState } from 'react'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { router } from 'expo-router'; import type { MobileNote } from '../../api/notes'; +import type { MobileWorkspace } from '../../api/workspaces'; import { useNotesStore, type NotesState } from '../../store/notes-store'; +import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store'; import { colors } from '../../theme'; export default function SearchScreen() { const [query, setQuery] = useState(''); const notes = useNotesStore((state: NotesState) => state.notes); + const workspaces = useWorkspaceStore((state: WorkspaceState) => state.workspaces); + const activeWorkspaceId = useWorkspaceStore((state: WorkspaceState) => state.activeWorkspaceId); + const activeWorkspaceName = + workspaces.find((workspace: MobileWorkspace) => workspace.id === activeWorkspaceId)?.name ?? null; const results = useMemo(() => { + const scopedNotes = notes.filter((note: MobileNote) => + activeWorkspaceName ? note.workspaceName === activeWorkspaceName : true + ); + if (!query.trim()) { - return notes; + return scopedNotes; } const normalized = query.toLowerCase(); - return notes.filter((note: MobileNote) => note.title.toLowerCase().includes(normalized)); - }, [notes, query]); + return scopedNotes.filter( + (note: MobileNote) => + note.title.toLowerCase().includes(normalized) || + note.body.toLowerCase().includes(normalized) || + note.workspaceName.toLowerCase().includes(normalized) + ); + }, [activeWorkspaceName, notes, query]); return ( Search + Scope: {activeWorkspaceName ?? 'All workspaces'} void; reject: (id: string) => void; }; @@ -28,6 +36,26 @@ export const useInboxStore = create((set) => ({ status: 'pending', }, ], + activity: [ + { + id: 'activity-1', + title: 'Summary refreshed', + summary: 'Agent updated a note summary after recent edits.', + kind: 'agent', + }, + { + id: 'activity-2', + title: 'Task extracted', + summary: 'A follow-up task was extracted from a meeting note.', + kind: 'task', + }, + { + id: 'activity-3', + title: 'Related note linked', + summary: 'A planning note was linked to a roadmap note.', + kind: 'note', + }, + ], approve(id: string) { set((state) => ({ approvals: state.approvals.map((item) => diff --git a/mobile/src/store/workspace-store.ts b/mobile/src/store/workspace-store.ts index d027f8d..dc5e8f8 100644 --- a/mobile/src/store/workspace-store.ts +++ b/mobile/src/store/workspace-store.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { listWorkspaces, type MobileWorkspace } from '../api/workspaces'; -type WorkspaceState = { +export type WorkspaceState = { workspaces: MobileWorkspace[]; activeWorkspaceId: string | null; hydrate: () => Promise;