# `app`'s `client` module `app/src/client` contains platform- and UI-independent client logic. It must not depend on iris; a `use iris::` below this directory is a layering defect. `event-model` remains a separate crate because the server and client both depend on that wire contract. ## Contents - `api.rs`: REST client over the injectable `Transport` trait. - `sse.rs` and `event_stream.rs`: SSE framing and session-event following. - `transcript_cache.rs`: bounded, persistent transcript chunks. - `transcript_source.rs`: cache/server selection and live cache updates. - `transcript_fold.rs`: event folding, tool grouping, and page healing. - `markdown_blocks.rs`, `ansi.rs`, and `highlight/`: display-independent text parsing and spans. - `config.rs`: enrollment-link parsing and the shared `EnrolledServer` value. - `log_ring.rs`: bounded process-local diagnostics. ## API coverage `ApiClient` covers session list/read, messages, unqueue, answers, interrupt, stop/start, rename, working directory, model, permission mode, notification setting, commands, compaction, deletion, and transcript pages. Still missing are setups and discovery, file operations, usage, models and downloads, attachments, imports, and the global notifications stream. `server/src/routes.rs` is the authoritative route table. ## Transcript invariants `join_pages` heals messages and tool runs split across page boundaries. It asserts that a tool id does not survive in both halves. It must not assert sequence ordering across the seam: a peer note carries the sequence of the turn it belongs above and can legitimately interleave with the page where it arrived. `Event` has no catch-all variant. A newer server adding an event type will make an older client reject that line rather than draw a placeholder. Fixing that requires a shared wire-model decision, not a client-only workaround. `TranscriptSource::page(0, ..)` returns `OlderPage::NothingLoaded` without touching cache or network. This is deliberately distinct from `OlderPage::Events(vec![])`, which means the start of the conversation was actually reached. Network and cache parse failures are errors for the same reason: none of these states may latch a caller's “no more history” flag. Fetched transcript lines retain the server's exact JSON bytes through `RawValue`. Re-serializing parsed JSON can change floating-point text, causing the cached and streamed forms of one event to disagree byte-for-byte. The reconnect/backoff loop and cancellation of a live stream belong to the embedding runtime. `TranscriptSource::follow` only guarantees that each frame is cached before the caller receives it. ## Enrollment `EnrolledServer` and `parse_link` understand the same `aiapp://enroll?host=H&port=P&token=T[&ca=B]` value used by Android. Storage is caller-specific: Android uses its platform storage and the desktop writes a 0600 file under its XDG config directory. ## Markdown scope `markdown_blocks` splits top-level headings, paragraphs, fences, lists, tables, and quotes. It intentionally does not build a full nested CommonMark AST; inline styling and nested presentation remain renderer concerns until a shared non-UI consumer needs them. ## Verification Run `./scripts/run-tests.sh` from the repository root. For this crate alone, run `cargo test`, `cargo clippy --all-targets`, and `cargo fmt --check` from `app/`.