summaryrefslogtreecommitdiff
path: root/apps/mobile/hooks
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-18 17:44:50 +0000
committerGitHub <noreply@github.com>2026-01-18 17:44:50 +0000
commit869f21ee2efaefed6a5aa4fbd417c25df8dec02a (patch)
tree2a90820ac817173e5b7154e0ba5e4f5d095f9613 /apps/mobile/hooks
parent219bca168508e1ea5e91e8a9ce98338afeddfbd2 (diff)
downloadsoryu-869f21ee2efaefed6a5aa4fbd417c25df8dec02a.tar.gz
soryu-869f21ee2efaefed6a5aa4fbd417c25df8dec02a.zip
Add React Native mobile app for Makima (#3)
* [WIP] Heartbeat checkpoint - 2026-01-18 02:58:27 UTC * feat(mobile): complete mobile app integration and verification - Add ThemeColors type export to Colors.ts for type safety - Export SUPABASE_URL from supabase.ts and use environment variables - Update .env.example with correct default URLs - Add comprehensive README.md with setup instructions Verified: - TypeScript compiles without errors - App exports successfully for iOS and Android - All screens accessible (login, dashboard, tasks, settings, task detail) - Auth flow working with Zustand store and Supabase Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'apps/mobile/hooks')
-rw-r--r--apps/mobile/hooks/index.ts9
-rw-r--r--apps/mobile/hooks/useColorScheme.ts5
-rw-r--r--apps/mobile/hooks/useQuestions.ts44
-rw-r--r--apps/mobile/hooks/useTasks.ts204
-rw-r--r--apps/mobile/hooks/useThemeColor.ts47
5 files changed, 309 insertions, 0 deletions
diff --git a/apps/mobile/hooks/index.ts b/apps/mobile/hooks/index.ts
new file mode 100644
index 0000000..2fa97fc
--- /dev/null
+++ b/apps/mobile/hooks/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Hooks exports
+ */
+export {
+ useThemeColors,
+ useThemeColor,
+ useThemeColorSet,
+ useIsDarkMode,
+} from './useThemeColor';
diff --git a/apps/mobile/hooks/useColorScheme.ts b/apps/mobile/hooks/useColorScheme.ts
new file mode 100644
index 0000000..97ef339
--- /dev/null
+++ b/apps/mobile/hooks/useColorScheme.ts
@@ -0,0 +1,5 @@
+import { useColorScheme as useRNColorScheme } from 'react-native';
+
+export function useColorScheme() {
+ return useRNColorScheme() ?? 'light';
+}
diff --git a/apps/mobile/hooks/useQuestions.ts b/apps/mobile/hooks/useQuestions.ts
new file mode 100644
index 0000000..af77a51
--- /dev/null
+++ b/apps/mobile/hooks/useQuestions.ts
@@ -0,0 +1,44 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { listPendingQuestions, answerQuestion, type PendingQuestion } from '../lib/api';
+
+// Query keys for questions
+export const questionKeys = {
+ all: ['questions'] as const,
+ lists: () => [...questionKeys.all, 'list'] as const,
+ list: () => [...questionKeys.lists()] as const,
+};
+
+/**
+ * Hook to fetch pending questions
+ * Polls every 5 seconds for updates
+ */
+export function usePendingQuestions() {
+ return useQuery({
+ queryKey: questionKeys.list(),
+ queryFn: listPendingQuestions,
+ refetchInterval: 5000,
+ });
+}
+
+/**
+ * Hook to answer a pending question
+ */
+export function useAnswerQuestion() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ questionId, response }: { questionId: string; response: string }) =>
+ answerQuestion(questionId, response),
+ onSuccess: () => {
+ // Invalidate questions list to refetch
+ queryClient.invalidateQueries({ queryKey: questionKeys.lists() });
+ },
+ });
+}
+
+/**
+ * Get the count of pending questions
+ */
+export function getQuestionCount(questions: PendingQuestion[] | undefined): number {
+ return questions?.length ?? 0;
+}
diff --git a/apps/mobile/hooks/useTasks.ts b/apps/mobile/hooks/useTasks.ts
new file mode 100644
index 0000000..4d56f63
--- /dev/null
+++ b/apps/mobile/hooks/useTasks.ts
@@ -0,0 +1,204 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ listTasks,
+ getTask,
+ startTask,
+ stopTask,
+ getTaskOutput,
+ sendTaskMessage,
+ type TaskSummary,
+ type TaskWithSubtasks,
+ type TaskOutputResponse,
+} from '../lib/api';
+
+// Query keys for consistent cache management
+export const taskKeys = {
+ all: ['tasks'] as const,
+ lists: () => [...taskKeys.all, 'list'] as const,
+ list: () => [...taskKeys.lists()] as const,
+ details: () => [...taskKeys.all, 'detail'] as const,
+ detail: (id: string) => [...taskKeys.details(), id] as const,
+ output: (id: string) => [...taskKeys.all, 'output', id] as const,
+};
+
+/**
+ * Hook to fetch the list of all tasks
+ * Automatically refetches every 5 seconds for live updates
+ */
+export function useTasks() {
+ return useQuery({
+ queryKey: taskKeys.list(),
+ queryFn: async () => {
+ const response = await listTasks();
+ return response.tasks;
+ },
+ refetchInterval: 5000, // Poll every 5 seconds for updates
+ staleTime: 2000, // Consider data stale after 2 seconds
+ });
+}
+
+/**
+ * Hook to fetch a specific task with its subtasks
+ */
+export function useTask(taskId: string | null) {
+ return useQuery({
+ queryKey: taskKeys.detail(taskId ?? ''),
+ queryFn: () => getTask(taskId!),
+ enabled: !!taskId,
+ refetchInterval: 5000,
+ });
+}
+
+/**
+ * Hook to fetch task output history
+ */
+export function useTaskOutput(taskId: string | null) {
+ return useQuery({
+ queryKey: taskKeys.output(taskId ?? ''),
+ queryFn: () => getTaskOutput(taskId!),
+ enabled: !!taskId,
+ refetchInterval: 3000, // More frequent updates for output
+ });
+}
+
+/**
+ * Hook to start a task
+ */
+export function useStartTask() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: startTask,
+ onSuccess: (updatedTask) => {
+ // Invalidate task list to refetch
+ queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
+ // Update the specific task in cache
+ queryClient.setQueryData(
+ taskKeys.detail(updatedTask.id),
+ (old: TaskWithSubtasks | undefined) => {
+ if (old) {
+ return { ...old, ...updatedTask };
+ }
+ return old;
+ }
+ );
+ },
+ });
+}
+
+/**
+ * Hook to stop a task
+ */
+export function useStopTask() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: stopTask,
+ onSuccess: (updatedTask) => {
+ // Invalidate task list to refetch
+ queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
+ // Update the specific task in cache
+ queryClient.setQueryData(
+ taskKeys.detail(updatedTask.id),
+ (old: TaskWithSubtasks | undefined) => {
+ if (old) {
+ return { ...old, ...updatedTask };
+ }
+ return old;
+ }
+ );
+ },
+ });
+}
+
+/**
+ * Hook to send a message to a task
+ */
+export function useSendTaskMessage() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ taskId, message }: { taskId: string; message: string }) =>
+ sendTaskMessage(taskId, message),
+ onSuccess: (_, { taskId }) => {
+ // Invalidate task output to refetch
+ queryClient.invalidateQueries({ queryKey: taskKeys.output(taskId) });
+ },
+ });
+}
+
+/**
+ * Helper to group tasks by status for display
+ */
+export function groupTasksByStatus(tasks: TaskSummary[]) {
+ const groups = {
+ running: [] as TaskSummary[],
+ pending: [] as TaskSummary[],
+ blocked: [] as TaskSummary[],
+ completed: [] as TaskSummary[],
+ };
+
+ for (const task of tasks) {
+ switch (task.status) {
+ case 'running':
+ case 'initializing':
+ case 'starting':
+ groups.running.push(task);
+ break;
+ case 'pending':
+ groups.pending.push(task);
+ break;
+ case 'blocked':
+ case 'paused':
+ groups.blocked.push(task);
+ break;
+ case 'done':
+ case 'failed':
+ case 'merged':
+ groups.completed.push(task);
+ break;
+ }
+ }
+
+ return groups;
+}
+
+/**
+ * Get counts for dashboard display
+ */
+export function getTaskCounts(tasks: TaskSummary[]) {
+ const counts = {
+ total: tasks.length,
+ running: 0,
+ pending: 0,
+ blocked: 0,
+ completed: 0,
+ failed: 0,
+ };
+
+ for (const task of tasks) {
+ switch (task.status) {
+ case 'running':
+ case 'initializing':
+ case 'starting':
+ counts.running++;
+ break;
+ case 'pending':
+ counts.pending++;
+ break;
+ case 'blocked':
+ case 'paused':
+ counts.blocked++;
+ break;
+ case 'done':
+ case 'merged':
+ counts.completed++;
+ break;
+ case 'failed':
+ counts.failed++;
+ break;
+ }
+ }
+
+ return counts;
+}
diff --git a/apps/mobile/hooks/useThemeColor.ts b/apps/mobile/hooks/useThemeColor.ts
new file mode 100644
index 0000000..170a6d6
--- /dev/null
+++ b/apps/mobile/hooks/useThemeColor.ts
@@ -0,0 +1,47 @@
+import { useColorScheme } from 'react-native';
+import { Colors, type ThemeColors } from '../constants/Colors';
+
+/**
+ * Hook to get the current theme colors
+ * @returns The colors for the current color scheme
+ */
+export function useThemeColors(): ThemeColors {
+ const colorScheme = useColorScheme() ?? 'dark';
+ return Colors[colorScheme];
+}
+
+/**
+ * Hook to get a specific color from the theme
+ * @param colorName - The name of the color to retrieve
+ * @returns The color value for the current theme
+ */
+export function useThemeColor<K extends keyof ThemeColors>(
+ colorName: K
+): ThemeColors[K] {
+ const colors = useThemeColors();
+ return colors[colorName];
+}
+
+/**
+ * Hook to get multiple colors from the theme
+ * @param colorNames - Array of color names to retrieve
+ * @returns Object with requested color values
+ */
+export function useThemeColorSet<K extends keyof ThemeColors>(
+ colorNames: K[]
+): Pick<ThemeColors, K> {
+ const colors = useThemeColors();
+ return colorNames.reduce((acc, name) => {
+ acc[name] = colors[name];
+ return acc;
+ }, {} as Pick<ThemeColors, K>);
+}
+
+/**
+ * Hook to check if the current theme is dark
+ * @returns boolean indicating if dark mode is active
+ */
+export function useIsDarkMode(): boolean {
+ const colorScheme = useColorScheme();
+ return colorScheme === 'dark';
+}