Parent directory

AGENTS.md

4989 bytes

kaiwari — iOS client

IMPORTANT

  • Never edit generated files: xtool/ (including xtool/kaiwari.app/Info.plist) and .build/ are wiped and regenerated on every build. The Info.plist source of truth is Resources/Info.plist, merged at build time via infoPath in xtool.yml.
  • Do not write any tests; there is no test suite in this project.
  • Never start, stop, or restart the live model services (llama-server, audiocpp_server) or kaiwari-server, and never load models into VRAM. They are user-managed; integration testing is manual and user-run only.

Architecture

Kaiwari is a spoken-Japanese learning game. This repo is the thin iOS client for kaiwari-server (sibling repo ../server). The app is a terminal for the game: it renders the adventure log, captures push-to-talk audio, and plays NPC speech. All game logic, ASR, judging, and TTS live on the server; the client's only state is the session and UI phase.

Tech stack: SwiftUI (Liquid Glass), Swift 6, SPM + xtool (no Xcode project), AVFoundation (record/play), URLSession.

Layers:

  • Wire: APIClient.swift must match ../server/internal/server/handlers.go exactly (DTOs, endpoints, bearer auth). TurnResponse is a flat struct = log entry fields + location/talk/audio/speakError/judgeError. Kana never crosses the wire; the UI is romaji-only by construction. Plain HTTP is intentional (VPN-only use): scheme check in kaiwariShared/ServerConfiguration.swift, ATS opt-out (NSAllowsArbitraryLoads) in Resources/Info.plist.
  • State: GameStore (@Observable @MainActor) owns the phase machine idle/recording/processing: session create + client-fired opening turn "Look around.", reconnect on launch via persisted session id (UserDefaults) + log fetch (any failure = fresh start), 15 s record cap with auto-stop. Bare ?/! prefixes are rejected and keep the input buffer (TUI parity).
  • UI: GameView (bottom-pinned log list, empty state, toolbar: replay / new adventure / settings, Liquid Glass input bar) and EntryView (per-turn card: action, desc, romaji + translation reveal chips, judge block, ask block).
  • Audio: Recorder → 16 kHz mono 16-bit WAV temp file (~480 KB at the cap, under the server's 2 MB upload limit). Player auto-plays the NPC line on turn completion; replay serves the client-side cache (the server only sends audio in turn responses, never in logs).
  • Config: SettingsStore + KeychainStore; server URL and bearer token, token in the device keychain.

Project Structure

ios/
├── Package.swift              # SPM manifest; deployment targets (iOS/macOS 26)
├── xtool.yml                  # bundleID, Info.plist path
├── Resources/Info.plist       # plist source of truth (ATS, mic usage string)
└── Sources/
    ├── kaiwari/               # app target
    │   ├── KaiwariApp.swift   # @main entry
    │   ├── ContentView.swift  # settings gate → game
    │   ├── SettingsStore.swift / SettingsView.swift
    │   ├── KeychainStore.swift
    │   ├── GameStore.swift    # phase machine, session lifecycle
    │   ├── GameView.swift     # log list, toolbar, input bar
    │   ├── EntryView.swift    # per-turn card with reveal chips
    │   ├── Recorder.swift / Player.swift
    │   └── APIClient.swift    # wire DTOs + HTTP client
    └── kaiwariShared/
        └── ServerConfiguration.swift  # URL/token, scheme check

Development Workflow

  • Build/verify: xtool dev build from this directory. Zero warnings is the bar. There is no simulator on this host (Linux cross-compile); visual and audio behavior are verified manually by the user on device.
  • Identity: bundle id lives in xtool.yml, keychain service name in KeychainStore.swift. Changing either orphans installed-app state (reinstall / one-time reconfigure).
  • Liquid Glass (.glassEffect, GlassEffectContainer, .buttonStyle(.glass...)) requires the iOS 26 deployment target — keep both platform entries in Package.swift at 26.0. Note: a glass effect applied to a button label only hit-tests the icon glyph; add .contentShape(.interaction, <shape>) covering the full control so the whole control is tappable.
  • Server contract: after changing anything in ../server/internal/server/handlers.go, port the DTO/endpoint changes into APIClient.swift.

Subagents

  • Run at most one worker subagent at a time, sequenced; never spawn two in parallel.
  • Sequence work in dependency-ordered batches; wait for each agent to finish before starting the next.
  • Give workers a plain-language description of the change (goal, behavior, constraints, how to verify), not a full file to transcribe. The main context does the design and thinking; the worker turns that into code.
  • Workers commit their changes when done: one commit per batch with a short, polished message describing the change.