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;