summaryrefslogtreecommitdiff
path: root/apps/mobile/lib/api.ts
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/lib/api.ts
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/lib/api.ts')
-rw-r--r--apps/mobile/lib/api.ts289
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();
+}