summaryrefslogblamecommitdiff
path: root/makima/ios/docs/ios-v1-plan.md
blob: 876a6ff439ecec54ca69bdb99b99c0b7b916dc8c (plain) (tree)





























































































































































































































                                                                                                                                                                                                                                                                                                                                                                                                                               
# 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 `<server>/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: <full-key>`** (constant `API_KEY_HEADER` line 404). Keys are prefixed `mk_`.
- **Supabase JWTs → header `Authorization: Bearer <jwt>`** (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://<server>/api/v1/mesh/tasks/subscribe`
Headers on the upgrade request: `x-makima-api-key: <full-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 `<COMPLETION_GATE>` 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: <mk_…>` — **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.