diff options
| author | soryu <soryu@soryu.co> | 2026-01-18 17:44:50 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-18 17:44:50 +0000 |
| commit | 869f21ee2efaefed6a5aa4fbd417c25df8dec02a (patch) | |
| tree | 2a90820ac817173e5b7154e0ba5e4f5d095f9613 /apps/mobile/lib/api.ts | |
| parent | 219bca168508e1ea5e91e8a9ce98338afeddfbd2 (diff) | |
| download | soryu-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/lib/api.ts')
| -rw-r--r-- | apps/mobile/lib/api.ts | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/apps/mobile/lib/api.ts b/apps/mobile/lib/api.ts new file mode 100644 index 0000000..ec848b6 --- /dev/null +++ b/apps/mobile/lib/api.ts @@ -0,0 +1,289 @@ +import { supabase } from './supabase'; + +// ============================================================================= +// API Configuration +// ============================================================================= + +const API_CONFIG = { + local: { + http: 'http://localhost:8080', + ws: 'ws://localhost:8080', + }, + production: { + http: 'https://api.makima.jp', + ws: 'wss://api.makima.jp', + }, +} as const; + +type Environment = 'local' | 'production'; + +// Detect environment based on __DEV__ flag +const env: Environment = __DEV__ ? 'local' : 'production'; + +export const API_BASE = API_CONFIG[env].http; +export const WS_BASE = API_CONFIG[env].ws; + +export function getEnvironment(): Environment { + return env; +} + +// ============================================================================= +// Authentication Helper +// ============================================================================= + +/** + * Fetch wrapper that automatically adds authentication headers + * Gets the access token from Supabase session + */ +async function authFetch(url: string, options: RequestInit = {}): Promise<Response> { + const { data: { session } } = await supabase.auth.getSession(); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (session?.access_token) { + headers['Authorization'] = `Bearer ${session.access_token}`; + } + + return fetch(`${API_BASE}${url}`, { + ...options, + headers: { ...headers, ...options.headers }, + }); +} + +// ============================================================================= +// Mesh/Task Types +// ============================================================================= + +export type TaskStatus = + | 'pending' + | 'initializing' + | 'starting' + | 'running' + | 'paused' + | 'blocked' + | 'done' + | 'failed' + | 'merged'; + +export type MergeMode = 'pr' | 'auto' | 'manual'; +export type CompletionAction = 'none' | 'branch' | 'merge' | 'pr'; +export type ContractPhase = 'research' | 'specify' | 'plan' | 'execute' | 'review'; + +export interface TaskSummary { + id: string; + contractId: string | null; + contractName: string | null; + contractPhase: ContractPhase | null; + parentTaskId: string | null; + depth: number; + name: string; + status: TaskStatus; + priority: number; + progressSummary: string | null; + subtaskCount: number; + isSupervisor: boolean; + version: number; + createdAt: string; + updatedAt: string; +} + +export interface Task { + id: string; + ownerId: string; + contractId: string | null; + parentTaskId: string | null; + depth: number; + name: string; + description: string | null; + status: TaskStatus; + priority: number; + plan: string; + daemonId: string | null; + containerId: string | null; + overlayPath: string | null; + repositoryUrl: string | null; + baseBranch: string | null; + targetBranch: string | null; + mergeMode: MergeMode | null; + prUrl: string | null; + targetRepoPath: string | null; + completionAction: CompletionAction | null; + progressSummary: string | null; + lastOutput: string | null; + errorMessage: string | null; + startedAt: string | null; + completedAt: string | null; + version: number; + createdAt: string; + updatedAt: string; + isSupervisor: boolean; +} + +export interface TaskWithSubtasks extends Task { + subtasks: TaskSummary[]; +} + +export interface TaskListResponse { + tasks: TaskSummary[]; + total: number; +} + +export interface TaskOutputEntry { + id: string; + taskId: string; + messageType: string; + content: string; + toolName?: string; + toolInput?: Record<string, unknown>; + isError?: boolean; + costUsd?: number; + durationMs?: number; + createdAt: string; +} + +export interface TaskOutputResponse { + entries: TaskOutputEntry[]; + total: number; + taskId: string; +} + +export interface SendMessageResponse { + success: boolean; + taskId: string; + messageLength: number; +} + +// ============================================================================= +// Pending Question Types +// ============================================================================= + +export interface PendingQuestion { + questionId: string; + taskId: string; + contractId: string; + question: string; + choices: string[]; + context: string | null; + createdAt: string; +} + +export interface AnswerQuestionResponse { + success: boolean; +} + +// ============================================================================= +// Task API Functions +// ============================================================================= + +/** + * List all tasks for the current user + */ +export async function listTasks(): Promise<TaskListResponse> { + const res = await authFetch('/api/v1/mesh/tasks'); + if (!res.ok) { + throw new Error(`Failed to list tasks: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get a specific task with its subtasks + */ +export async function getTask(id: string): Promise<TaskWithSubtasks> { + const res = await authFetch(`/api/v1/mesh/tasks/${id}`); + if (!res.ok) { + throw new Error(`Failed to get task: ${res.statusText}`); + } + return res.json(); +} + +/** + * Start a pending task + */ +export async function startTask(id: string): Promise<Task> { + const res = await authFetch(`/api/v1/mesh/tasks/${id}/start`, { + method: 'POST', + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to start task: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Stop a running task + */ +export async function stopTask(id: string): Promise<Task> { + const res = await authFetch(`/api/v1/mesh/tasks/${id}/stop`, { + method: 'POST', + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to stop task: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Send a message to a running task's stdin + */ +export async function sendTaskMessage( + taskId: string, + message: string +): Promise<SendMessageResponse> { + const res = await authFetch(`/api/v1/mesh/tasks/${taskId}/message`, { + method: 'POST', + body: JSON.stringify({ message }), + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to send message: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Get task output history + */ +export async function getTaskOutput(taskId: string): Promise<TaskOutputResponse> { + const res = await authFetch(`/api/v1/mesh/tasks/${taskId}/output`); + if (!res.ok) { + throw new Error(`Failed to get task output: ${res.statusText}`); + } + return res.json(); +} + +// ============================================================================= +// Question API Functions +// ============================================================================= + +/** + * Get all pending supervisor questions for the current user + */ +export async function listPendingQuestions(): Promise<PendingQuestion[]> { + const res = await authFetch('/api/v1/mesh/questions'); + if (!res.ok) { + throw new Error(`Failed to list questions: ${res.statusText}`); + } + return res.json(); +} + +/** + * Answer a pending supervisor question + */ +export async function answerQuestion( + questionId: string, + response: string +): Promise<AnswerQuestionResponse> { + const res = await authFetch(`/api/v1/mesh/questions/${questionId}/answer`, { + method: 'POST', + body: JSON.stringify({ response }), + }); + if (!res.ok) { + throw new Error(`Failed to answer question: ${res.statusText}`); + } + return res.json(); +} |
