# Makima iOS — v1 Plan **Date:** 2026-04-24 **Target:** iOS 18.0+ · iPhone only · Pure native SwiftUI · No Android **Upstream:** `soryu/makima` (Rust + Postgres server, React frontend at `makima.jp`) **Location:** new folder `apps/ios/` inside the existing `soryu-co/soryu` monorepo (sibling to `frontend/`, `makima/`, `k8s/`). **License:** MIT (carry repo-level LICENSE; no per-folder LICENSE needed). **App Store seller:** Soryu LTD. **Bundle ID:** `co.soryu.makima` (dropped the `.ios` suffix per preference). --- ## 1. Product shape **Primary surface: Composite Home.** A glanceable dashboard with section cards: - **Active Contracts** — count + top 3 running, tap → Contracts list - **Active Daemons** — mesh status pill (N online / M offline) - **Pending Directive Questions** — amber badge, tap → Directives - **Latest Listen Session** — last transcript preview, tap → Listen detail - **Recent Tasks** — 3 most recent tasks across contracts, tap → Task detail (live output) Secondary surfaces reachable from home: Contracts list, Tasks list, Directives, Daemons, Listen (read-only history in v1), Settings. **Not in v1:** Speak (TTS), Files editor, Orders write-path, mesh merge UI, worktree diff viewer, daemon reauth flow, supervisor questions *answering* (read-only badge only — answer on web). ## 2. Stack - **Language:** Swift 5.10+ / SwiftUI - **Min iOS:** 18.0 (same as Litter/KittyLitter — lets us use latest SwiftUI APIs, Observation, Swift Concurrency) - **Architecture:** MV (Model + View) with `@Observable` stores — no MVVM ceremony - **Networking:** `URLSession` + `URLSessionWebSocketTask` (no Alamofire/Starscream) - **Persistence:** SwiftData for cached contracts/tasks/transcripts; Keychain for API key(s); `UserDefaults` for server-profile list - **Markdown/code:** `swift-markdown-ui` (MIT) for markdown, `Splash` or `Runestone` for syntax highlighting (evaluate at impl time — Splash is lighter, Runestone handles big files) - **Push:** APNs via a small `services/push-proxy` later — **not in v1**. v1 uses local notifications triggered by WS events while app is foreground/recent. True background push = v1.1. - **Build:** XcodeGen (`project.yml` source of truth, matches Litter) - **No dependencies beyond:** swift-markdown-ui, KeychainAccess (or hand-roll), a syntax highlighter. Everything else stdlib. ## 3. Auth & server configuration ### 3.1 Multi-profile server picker *(question 3 = a: single field, but we're building the primitive that generalises)* User picked (a) — single "Server URL" field. Implementation: - `Settings → Server URL` text field, default `https://makima.jp` - Stored in Keychain alongside the API key (so swapping server swaps the credential bundle) - On change: clear cached contracts/tasks/daemons, re-fetch `/health`, prompt for new API key if 401 - Internally represented as `ServerProfile { url: URL, apiKey: String, label: String }` — list-of-one in v1, lets us upgrade to multi-profile in v1.x without migration ### 3.2 API key flow *(question 2 = c: both — but v1 = API key only)* User said (c) both, but Supabase OAuth needs project keys and deep-link wiring. Ship API key paste in v1, add Supabase in v1.1: 1. First-launch onboarding: - Screen 1: Server URL (prefilled `https://makima.jp`) - Screen 2: "Open web settings → create API key → paste here" with a `Open in Safari` button to `/settings` - Screen 3: validate via `GET /api/v1/mesh/daemons` (any authenticated endpoint); on success, store and proceed to Home 2. API key stored in Keychain, scoped per-server-URL 3. `Settings → Rotate key` button — calls `POST /api/v1/auth/api-keys/refresh`, replaces stored key ### 3.3 Auth header *(verified 2026-04-24 against `src/server/auth.rs`)* - **API keys → header `x-makima-api-key: `** (constant `API_KEY_HEADER` line 404). Keys are prefixed `mk_`. - **Supabase JWTs → header `Authorization: Bearer `** (line 1039–1041). - Order of precedence in the server: tool key → API key → JWT. - **v1 sends `x-makima-api-key` only.** `Authorization: Bearer` is reserved for v1.1 Supabase OAuth. Don't mix. - WebSocket: same header works via `URLSessionWebSocketTask` custom request headers (not via `?token=` query). Open Q #7 resolved — update client code accordingly. ## 4. Surfaces & endpoints | Surface | Endpoints (v1) | Notes | |---|---|---| | Home | `GET /mesh/tasks?status=running`, `GET /mesh/daemons`, `GET /directives?pending=true`, `GET /listen/sessions?limit=1` | Parallel fetch, 30s refresh, pull-to-refresh | | Contracts list | `GET /contracts` | Filter: active / completed / archived | | Contract detail | `GET /contracts/{id}`, `GET /mesh/tasks?contract_id={id}` | Shows phase, tasks | | Tasks list | `GET /mesh/tasks` | Filter by status, contract | | **Task detail (hero)** | `GET /mesh/tasks/{id}`, `GET /mesh/tasks/{id}/output`, `WS /mesh/tasks/subscribe` | Live output stream, markdown+code rendered | | Directives | `GET /directives` | Read-only; badge for pending questions | | Daemons | `GET /mesh/daemons` | Status list, read-only | | Listen (history) | `GET /listen/sessions`, `GET /listen/sessions/{id}` | Transcript viewer, no live mic in v1 | | Settings | `GET /auth/api-keys`, `POST /auth/api-keys/refresh`, `DELETE /auth/api-keys` | Server URL, key rotate, about | ### 4.1 WebSocket: Task output livestream *(question 4)* Single WS: `wss:///api/v1/mesh/tasks/subscribe` Headers on the upgrade request: `x-makima-api-key: ` (same as HTTP — verified above). - Subscribes to all tasks the user has access to - Server pushes task events: `task.output`, `task.status`, `task.completion_gate`, `task.supervisor_question` - App maintains in-memory buffer per visible task, SwiftData-persists last 2000 lines on background - Auto-reconnect with exponential backoff (1s → 30s cap), shown as pill in masthead - When app is foregrounded, send `resync` frame with last-seen event IDs per task to catch up ## 5. Notifications *(question 5 must-have)* **v1 (local only):** - While app is open or recently backgrounded (iOS background WS grace ~30s), fire `UNUserNotificationCenter` local notifications for: - Task completed - Task failed - Supervisor question arrived - Notification taps deep-link to the relevant surface (`makima://task/{id}`, `makima://directive/{id}`) **v1.1 (push):** - Small `services/push-proxy` service (own repo or monorepo folder) that holds APNs device tokens and subscribes to the same WS on behalf of users - Server emits "notify" events; proxy fans out to APNs - Matches Litter's `services/push-proxy/` pattern ## 6. Aesthetic port *(question 5 must-have)* Goal: feel like a native extension of makima.jp, not a generic iOS app. - **Colors:** port exactly — `#0c1729` bg, `#9bc3ff` foreground, `#3f6fb3` accent, amber `#f59e0b` for alerts. Central `Palette.swift`. - **Typography:** SF Mono for all chrome (nav, labels, ticker), SF Pro for body. Uppercase + tracked for nav labels (matches NavStrip). - **Chrome elements to port:** - `NAV//` prefix strip → iOS tab bar replaced with a custom masthead bar (top), tab-bar-less - Dashed borders (`border-dashed` in Tailwind → `StrokeStyle(dash: [4, 4])` in SwiftUI) - Masthead ticker → a thin top bar with build version + WS status + server label - **Logo:** reuse the existing logo from the web frontend — `frontend/public/logo/makima-logo.svg` (concentric rings). Copy into `apps/ios/Sources/Makima/Resources/Logo/` at M1, render as SwiftUI `Image` with SVG support via a tiny helper (SwiftUI 18 has native SVG; fallback = rasterize to PDF asset at build time). Also port `crane-logo-transparent.png` for the Soryu masthead credit if the design calls for it. - `JapaneseHoverText` → `JapaneseLongPressText` on iOS (long-press reveals English). Same term list: 支配する, 命令, 契約, 史料, 聴取, etc. - **Motion:** respectful — fades and springs only. No parallax, no bounce. ## 7. Markdown & code rendering *(question 5 must-have)* - `swift-markdown-ui` with custom theme matching web: inline code in `#1a2840` pill, code fences with language label chip, tables scroll horizontally - Syntax highlighting via Splash (lightweight) or Runestone (full editor; overkill unless we add editing later) - Task output stream is treated as a sequence of markdown chunks with optional `` blocks rendered as a distinct status card (not raw XML) - Links are tappable; `file://` or repo-relative links show a "not supported on mobile" toast rather than erroring ## 8. Data model (client-side SwiftData) ``` ServerProfile { url, label, apiKeyRef, lastSeenAt } Contract { id, name, phase, status, autonomous_loop, updated_at } Task { id, contract_id?, parent_id?, status, plan, daemon_id?, updated_at } TaskEvent { id, task_id, kind, payload_json, ts } // bounded by task, prune >2k Daemon { id, status, last_heartbeat, hostname? } Directive { id, title, status, has_pending_question, updated_at } ListenSession { id, title?, started_at, ended_at?, transcript_preview } ``` Everything else (file contents, full transcripts, mesh details) = fetch on demand, don't cache. ## 9. Repo layout — as a folder inside `soryu-co/soryu` ``` soryu/ # existing monorepo root apps/ ios/ # <— NEW project.yml # XcodeGen source Makefile # ios-device-fast, ios-sim-fast, bootstrap, lint Sources/ Makima/ App/ # MakimaApp, entry, scene Design/ # Palette, Typography, Components (DashedBorder, MastheadBar, JapaneseLongPressText, Logo) Net/ # APIClient, WebSocketClient, AuthStore, ServerProfileStore Stores/ # HomeStore, ContractsStore, TasksStore, DirectivesStore, DaemonsStore (all @Observable) Features/ Home/ # HomeView + section cards Contracts/ Tasks/ # TasksListView, TaskDetailView (hero), OutputStreamView Directives/ Daemons/ Listen/ # history only in v1 Settings/ Onboarding/ Models/ # Contract, Task, TaskEvent, Daemon, Directive, ListenSession Markdown/ # Theme, code block, completion gate block Resources/ Logo/ # copied from ../../../frontend/public/logo/ Assets.xcassets Tests/ MakimaTests/ # AuthStore, APIClient (URLProtocol stubs), WebSocketClient reconnect docs/ DEVELOPMENT.md RELEASING.md README.md # points back to repo-root LICENSE (MIT) frontend/ # existing makima/ # existing k8s/ # existing LICENSE # repo-root MIT (confirm exists; add if missing) README.md # add "apps/ios — Makima iOS client" line ``` ## 10. Milestones | M | Scope | Done-when | |---|---|---| | M0 | Repo scaffold, XcodeGen, CI (GitHub Actions `xcodebuild`), empty app launches | App opens to "Hello, Makima" on sim + device | | M1 | Design system: Palette, Typography, DashedBorder, MastheadBar, Logo (Canvas), JapaneseLongPressText | Standalone previews render; aesthetic matches web side-by-side | | M2 | Onboarding + ServerProfile + Keychain API key + APIClient + auth probe | Paste key, see 200 from `/mesh/daemons` | | M3 | Home composite view (static → wired) | 5 cards render from live endpoints, pull-to-refresh | | M4 | Contracts list + detail, Tasks list + detail (polled output first, no WS yet) | Can drill from Home → Contract → Task and see output | | M5 | WebSocket livestream + markdown/code rendering + completion gate card | Task detail updates without polling; completion gate shows as status card | | M6 | Directives read-only, Daemons read-only, Listen history | All nav items reachable | | M7 | Local notifications + deep links | Foregrounded WS fires notifications that deep-link correctly | | M8 | Polish pass + TestFlight | Review-ready build, screenshots, privacy copy | ## 11. Decisions locked (2026-04-24) | # | Decision | |---|---| | License | **MIT**, carried by repo-root `LICENSE` in `soryu-co/soryu` | | App Store seller | **Soryu LTD** | | Bundle ID | **`co.soryu.makima`** (no `.ios` suffix) | | Supabase OAuth | Deferred to **v1.1** | | Logo | **Reuse** existing `frontend/public/logo/makima-logo.svg` (no redraw) | | Privacy policy | Point to repo README (KittyLitter-style) | | Auth header | `x-makima-api-key: ` — **verified** against `src/server/auth.rs` | | Push-proxy (v1.1) | `soryu-co/soryu` repo, new folder **`services/push-proxy/`** | | Repo location | Folder **`apps/ios/`** inside existing `soryu-co/soryu` monorepo | All blockers resolved. M0 (scaffold + XcodeGen + CI) is unblocked. --- ## Cross-checks against user preferences - ✅ Zero-friction auth: single paste, then nothing. Supabase OAuth queued for v1.1. - ✅ Customizable API URL: single-profile today, multi-profile-ready internally. - ✅ Aesthetic-forward: dedicated M1 before any feature work. - ✅ License-clean: concentric-circle logo redrawn in Canvas, no character likeness — matches "symbolic/abstract" preference. No copyrighted assets. - ✅ Pure native: zero JS, zero RN, zero Expo, zero Rust-UniFFI.