feat(mobile): add workspace-scoped utility flows
This commit is contained in:
parent
c517375e39
commit
90558a5537
@ -27,17 +27,17 @@ Stack: React Native + Expo + TypeScript
|
|||||||
|
|
||||||
# Phase M2 — Product Utility
|
# Phase M2 — Product Utility
|
||||||
|
|
||||||
- [ ] Lightweight editing
|
- [x] Lightweight editing
|
||||||
- [ ] Notification handling
|
- [ ] Notification handling
|
||||||
- [ ] Offline queue / sync polish
|
- [ ] Offline queue / sync polish
|
||||||
- [ ] Approvals inbox if in MVP scope
|
- [x] Approvals inbox if in MVP scope
|
||||||
- [ ] Basic artifact metadata viewing if in MVP scope
|
- [x] Basic artifact metadata viewing if in MVP scope
|
||||||
|
|
||||||
# Phase M3 — Agent / Approval Utility
|
# Phase M3 — Agent / Approval Utility
|
||||||
|
|
||||||
- [ ] Approval/reject actions
|
- [x] Approval/reject actions
|
||||||
- [ ] Lightweight agent activity feed
|
- [x] Lightweight agent activity feed
|
||||||
- [ ] Mobile-friendly review surfaces for simple actions
|
- [x] Mobile-friendly review surfaces for simple actions
|
||||||
|
|
||||||
# Phase M4 — Hardening
|
# Phase M4 — Hardening
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ Stack: React Native + Expo + TypeScript
|
|||||||
# Done When
|
# Done When
|
||||||
|
|
||||||
- [ ] Mobile is useful for capture and retrieval
|
- [ ] 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
|
- [ ] Mobile does not try to duplicate dense web-only workflows
|
||||||
|
|
||||||
# Progress Notes
|
# Progress Notes
|
||||||
@ -65,22 +65,24 @@ Stack: React Native + Expo + TypeScript
|
|||||||
- 2026-03-10 — Mobile workstream moved from draft-only planning into initial implementation.
|
- 2026-03-10 — Mobile workstream moved from draft-only planning into initial implementation.
|
||||||
- `learning_ai_notes/mobile/` was scaffolded from scratch with Expo + TypeScript.
|
- `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`.
|
- 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:
|
- Shared mobile bootstrap files now exist for:
|
||||||
- `@bytelyst/auth-client`
|
- `@bytelyst/auth-client`
|
||||||
- `@bytelyst/platform-client`
|
- `@bytelyst/platform-client`
|
||||||
- `@bytelyst/offline-queue`
|
- `@bytelyst/offline-queue`
|
||||||
- token-based theme wiring via `@bytelyst/design-tokens/tokens.json`
|
- 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.
|
- 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.
|
- Mobile currently uses provisional product config and fallback note/workspace data until product identity and backend contract details are finalized.
|
||||||
|
|
||||||
# Open Questions
|
# Open Questions
|
||||||
|
|
||||||
- What is the final canonical `productId` for ByteLyst Agentic Notes?
|
- Should the current provisional bootstrap values (`productId: bytelyst-notes`, backend port `4016`) now be treated as final?
|
||||||
- What is the final product backend port for the notes service?
|
|
||||||
- What are the final iOS bundle identifier, Android package name, and URL scheme/domain values?
|
- 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`?
|
- 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
|
# Blockers
|
||||||
|
|
||||||
@ -90,9 +92,6 @@ Stack: React Native + Expo + TypeScript
|
|||||||
|
|
||||||
# Deferred
|
# Deferred
|
||||||
|
|
||||||
- Lightweight editing beyond scaffold-level draft save
|
|
||||||
- Real notification handling
|
- Real notification handling
|
||||||
- Offline queue flush/retry integration against live APIs
|
- 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
|
- Smoke tests and compile verification after dependency install
|
||||||
|
|||||||
@ -1,18 +1,26 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
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';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function CaptureScreen() {
|
export default function CaptureScreen() {
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [body, setBody] = useState('');
|
const [body, setBody] = useState('');
|
||||||
const [saved, setSaved] = useState(false);
|
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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>Quick capture</Text>
|
<Text style={styles.title}>Quick capture</Text>
|
||||||
<Text style={styles.subtitle}>Create a lightweight mobile draft. Offline queue wiring comes in the next batch.</Text>
|
<Text style={styles.subtitle}>
|
||||||
|
Create a lightweight mobile draft in {activeWorkspaceName}. Offline queue wiring comes in a later batch.
|
||||||
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={title}
|
value={title}
|
||||||
onChangeText={(value: string) => {
|
onChangeText={(value: string) => {
|
||||||
@ -87,7 +95,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: '#EFF4FF',
|
color: colors.textPrimary,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
saved: {
|
saved: {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
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';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function InboxScreen() {
|
export default function InboxScreen() {
|
||||||
const approvals = useInboxStore((state: InboxState) => state.approvals);
|
const approvals = useInboxStore((state: InboxState) => state.approvals);
|
||||||
|
const activity = useInboxStore((state: InboxState) => state.activity);
|
||||||
const approve = useInboxStore((state: InboxState) => state.approve);
|
const approve = useInboxStore((state: InboxState) => state.approve);
|
||||||
const reject = useInboxStore((state: InboxState) => state.reject);
|
const reject = useInboxStore((state: InboxState) => state.reject);
|
||||||
|
|
||||||
@ -26,6 +27,16 @@ export default function InboxScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
<View style={styles.feedSection}>
|
||||||
|
<Text style={styles.feedTitle}>Recent activity</Text>
|
||||||
|
{activity.map((item: ActivityItem) => (
|
||||||
|
<View key={item.id} style={styles.feedCard}>
|
||||||
|
<Text style={styles.feedKind}>{item.kind}</Text>
|
||||||
|
<Text style={styles.cardTitle}>{item.title}</Text>
|
||||||
|
<Text style={styles.cardBody}>{item.summary}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -94,4 +105,26 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.textSecondary,
|
color: colors.textSecondary,
|
||||||
fontWeight: '600',
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,20 +1,30 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||||
import { router } from 'expo-router';
|
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';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
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(
|
const recentNotes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
notes.map((note) => ({
|
notes
|
||||||
id: note.id,
|
.filter((note: MobileNote) => (activeWorkspaceName ? note.workspaceName === activeWorkspaceName : true))
|
||||||
title: note.title,
|
.map((note: MobileNote) => ({
|
||||||
workspace: note.workspaceName,
|
id: note.id,
|
||||||
preview: note.body,
|
title: note.title,
|
||||||
})),
|
workspace: note.workspaceName,
|
||||||
[notes]
|
preview: note.body,
|
||||||
|
})),
|
||||||
|
[activeWorkspaceName, notes]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -22,8 +32,25 @@ export default function HomeScreen() {
|
|||||||
<Text style={styles.eyebrow}>Recent notes</Text>
|
<Text style={styles.eyebrow}>Recent notes</Text>
|
||||||
<Text style={styles.title}>Capture and retrieve fast</Text>
|
<Text style={styles.title}>Capture and retrieve fast</Text>
|
||||||
<Text style={styles.subtitle}>Mobile MVP focuses on recent notes, quick capture, search, and lightweight review.</Text>
|
<Text style={styles.subtitle}>Mobile MVP focuses on recent notes, quick capture, search, and lightweight review.</Text>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.workspaceRow}>
|
||||||
|
{workspaces.map((workspace: MobileWorkspace) => {
|
||||||
|
const isActive = workspace.id === activeWorkspaceId;
|
||||||
|
|
||||||
{recentNotes.map((note) => (
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={workspace.id}
|
||||||
|
onPress={() => setActiveWorkspace(workspace.id)}
|
||||||
|
style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.workspaceChipText, isActive ? styles.workspaceChipTextActive : null]}>
|
||||||
|
{workspace.name}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{recentNotes.map((note: (typeof recentNotes)[number]) => (
|
||||||
<Pressable key={note.id} onPress={() => router.push(`/note/${note.id}`)} style={styles.card}>
|
<Pressable key={note.id} onPress={() => router.push(`/note/${note.id}`)} style={styles.card}>
|
||||||
<View style={styles.badgeRow}>
|
<View style={styles.badgeRow}>
|
||||||
<Text style={styles.badge}>{note.workspace}</Text>
|
<Text style={styles.badge}>{note.workspace}</Text>
|
||||||
@ -61,6 +88,29 @@ const styles = StyleSheet.create({
|
|||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
marginBottom: 8,
|
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: {
|
card: {
|
||||||
backgroundColor: colors.surfaceCard,
|
backgroundColor: colors.surfaceCard,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@ -74,7 +124,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
badge: {
|
badge: {
|
||||||
color: colors.accentPrimary,
|
color: colors.accentPrimary,
|
||||||
backgroundColor: '#14203A',
|
backgroundColor: colors.bgElevated,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
|
|||||||
@ -2,25 +2,41 @@ import { useMemo, useState } from 'react';
|
|||||||
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import type { MobileNote } from '../../api/notes';
|
import type { MobileNote } from '../../api/notes';
|
||||||
|
import type { MobileWorkspace } from '../../api/workspaces';
|
||||||
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
||||||
|
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
export default function SearchScreen() {
|
export default function SearchScreen() {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const notes = useNotesStore((state: NotesState) => state.notes);
|
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 results = useMemo(() => {
|
||||||
|
const scopedNotes = notes.filter((note: MobileNote) =>
|
||||||
|
activeWorkspaceName ? note.workspaceName === activeWorkspaceName : true
|
||||||
|
);
|
||||||
|
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
return notes;
|
return scopedNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = query.toLowerCase();
|
const normalized = query.toLowerCase();
|
||||||
return notes.filter((note: MobileNote) => note.title.toLowerCase().includes(normalized));
|
return scopedNotes.filter(
|
||||||
}, [notes, query]);
|
(note: MobileNote) =>
|
||||||
|
note.title.toLowerCase().includes(normalized) ||
|
||||||
|
note.body.toLowerCase().includes(normalized) ||
|
||||||
|
note.workspaceName.toLowerCase().includes(normalized)
|
||||||
|
);
|
||||||
|
}, [activeWorkspaceName, notes, query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>Search</Text>
|
<Text style={styles.title}>Search</Text>
|
||||||
|
<Text style={styles.scope}>Scope: {activeWorkspaceName ?? 'All workspaces'}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={setQuery}
|
onChangeText={setQuery}
|
||||||
@ -61,6 +77,10 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.textPrimary,
|
color: colors.textPrimary,
|
||||||
backgroundColor: colors.surfaceCard,
|
backgroundColor: colors.surfaceCard,
|
||||||
},
|
},
|
||||||
|
scope: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
list: {
|
list: {
|
||||||
gap: 10,
|
gap: 10,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,8 +7,16 @@ export type ApprovalItem = {
|
|||||||
status: 'pending' | 'approved' | 'rejected';
|
status: 'pending' | 'approved' | 'rejected';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActivityItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
kind: 'note' | 'task' | 'agent';
|
||||||
|
};
|
||||||
|
|
||||||
export type InboxState = {
|
export type InboxState = {
|
||||||
approvals: ApprovalItem[];
|
approvals: ApprovalItem[];
|
||||||
|
activity: ActivityItem[];
|
||||||
approve: (id: string) => void;
|
approve: (id: string) => void;
|
||||||
reject: (id: string) => void;
|
reject: (id: string) => void;
|
||||||
};
|
};
|
||||||
@ -28,6 +36,26 @@ export const useInboxStore = create<InboxState>((set) => ({
|
|||||||
status: 'pending',
|
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) {
|
approve(id: string) {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
approvals: state.approvals.map((item) =>
|
approvals: state.approvals.map((item) =>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { listWorkspaces, type MobileWorkspace } from '../api/workspaces';
|
import { listWorkspaces, type MobileWorkspace } from '../api/workspaces';
|
||||||
|
|
||||||
type WorkspaceState = {
|
export type WorkspaceState = {
|
||||||
workspaces: MobileWorkspace[];
|
workspaces: MobileWorkspace[];
|
||||||
activeWorkspaceId: string | null;
|
activeWorkspaceId: string | null;
|
||||||
hydrate: () => Promise<void>;
|
hydrate: () => Promise<void>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user