Files
ai-app/docs/CLIENT_CORE.md
T

8.7 KiB

client-core

client-core/ is the app's pure logic held once instead of twice, per RUST.md's recommendation item 1. It is a plain Rust library crate with no UI framework dependency of any kind, so it can outlive whichever one the app ends up drawing with (Masonry, iris, or something else -- see RUST.md). event-model/ is its sibling: the wire shape both this crate and server/ share, extracted from server/src/session/driver.rs and session/transcript.rs on 2026-09-04.

Neither crate is wired into anything yet. server/ re-exports event-model so its own behaviour is unchanged (./run-tests.sh covers it); client-core has no caller -- it exists for whichever experiment in RUST.md picks it up next (a Masonry or iris transcript screen, most likely).

What's here, and what Kotlin file it replaces

client-core/src/… Kotlin original Status
event-model/src/lib.rs (shared crate) Events.kt (the enum mirror) Done
ansi.rs Ansi.kt Done, ported test-for-test
highlight/mod.rs, languages.rs Highlighter.kt, Languages.kt Done, ported test-for-test
highlight/markdown.rs MarkdownSyntax.kt Done, ported test-for-test
transcript_cache.rs TranscriptCache.kt Done, ported test-for-test
sse.rs Sse.kt (the framing half) Done, new tests (Kotlin had none of its own beyond integration)
api.rs Api.kt Partial -- see below
event_stream.rs EventStream.kt Done
transcript_fold.rs TranscriptItems.kt, ToolRows.kt Partial -- see below
config.rs ServerConfig.kt's handleEnrollment New, desktop-only so far -- see below
(not started) TranscriptSource.kt Not started
(not ported, and may never be) TranscriptUnits.kt Out of scope -- see below

Every file above whose Kotlin counterpart had a JVM unit test (AnsiTest, HighlighterTest, TranscriptCacheTest) has had every one of those test cases ported alongside it, plus new tests for the pieces that had none (sse.rs, api.rs, event_stream.rs, transcript_fold.rs). Test count by crate as of this writing: 85 in client-core, 0 in event-model (its types carry no logic of their own to test -- server/'s own tests exercise them via session::transcript's round-trip coverage).

Correspondence notes worth knowing before touching either side

  • ansi.rs's StyledText/Style/Rgb stand in for Compose's AnnotatedString/SpanStyle/Color, since this crate has no Compose. StyledText is plain text plus a Vec<(Range<usize>, Style)> of non-overlapping spans. Whatever UI framework ends up consuming this crate maps Style onto its own text-styling type; nothing here should change to accommodate a particular one.
  • highlight's Span/Kind use char indices, not byte offsets (Vec<char> internally), mirroring the Kotlin original's Char-indexed strings. highlight::span_text turns a Span back into text for a caller working the same way; a caller that wants byte offsets into a &str has to convert.
  • transcript_cache.rs's SessionCache::guard found a real translation bug while it was being written: an early draft let a damaged chunk (one file unreadable, discard just this session) and a genuine I/O failure (disk gone, disable the whole cache) both surface as the same Err from one closure, which would have disabled every session's cache over a single corrupt chunk. Fixed by checking a thread-local "was this damage" flag before deciding which failure mode it was -- see the comment on guard and the commit message for transcript_cache.rs.

What api.rs covers, and what it does not yet

ApiClient wraps a Transport trait (network I/O kept out from behind, so ApiClient and event_stream::follow_session_events are tested with a fake transport and no server). UreqTransport is the only real implementation, backed by ureq -- see its Cargo.toml comment for why (blocking, already a project dependency, no extra TLS crate needed since ureq::tls::Certificate::from_pem reads the pinned CA directly).

Covered: session list/read, message send, unqueue, answer, interrupt, stop, start, rename, cwd, model, permission-mode, notify, command, compact, delete, and one transcript page.

Not covered, and each is real work rather than a stub to fill in: setups (/setups*, machine and provider discovery), the file explorer (/setups/{id}/dir|file), usage (/usage), models (/models*, HuggingFace browsing and downloads), attachments (/sessions/{id}/attachments), importing (/setups/{id}/importable*), and the /notifications stream. server/src/routes.rs's module doc is the full table to work from when one of these is next.

What transcript_fold.rs covers, and what it does not yet

fold_event covers every Event variant server/ can produce today, including tool-call/question/image attachment and peer-message placement. group_tool_runs groups adjacent calls into TranscriptRow::Tools.

Not ported: TranscriptItems.kt's joinPages (and its healSplitMessage/adoptRun helpers) -- the page-boundary healing that merges a tool call split across two fetched pages and re-merges a run a boundary cut through. This matters the moment paging backward through history is exercised; it is deliberately left rather than rushed, since it is exactly the kind of boundary logic this project's own "things that have bitten" section warns reads fine and is wrong at the edges.

Known gap, and a decision for whoever closes it: event_model::Event has no Unknown/catch-all variant, unlike Events.kt's hand-kept mirror. A server newer than this build that adds an event type will fail to parse that line rather than degrading to a placeholder row. Closing this means deciding how event_model itself represents "a shape I don't recognise" -- a shared-model decision affecting server/ too, not a client-core-only fix, so it is recorded here rather than silently worked around.

config.rs: EnrolledServer

EnrolledServer (host, port, bearer token) plus parse_link, which reads the exact aiapp://enroll?host=H&port=P&token=T deep link wg-app-link's enroll mints and ServerConfig.kt's handleEnrollment parses on the phone -- so any Rust client enrols from the same text a phone would scan as a QR, with no second format invented for it (RUST.md's E4, DECISIONS.md 2026-09-05). Deliberately does not decide where it is persisted or under what file permissions -- a phone seals its token in the Android Keystore, iris/desktop-app/src/config.rs writes it to $XDG_CONFIG_HOME/ai-app-desktop/enrollment.json at 0600 -- since that is caller-specific (the code rules' "ask for the least you need"). Its only caller today is desktop-app; a future Android build of this crate would be a second one, not a reason to move the type.

What is not started at all

  • TranscriptSource.kt -- the layer that decides whether a page comes from the transcript cache or the server, and stitches the two. Needs transcript_cache.rs and api.rs's transcript-page method, both of which exist now, so this is unblocked whenever picked up.
  • The markdown block model beyond syntax spans -- highlight/markdown.rs colours a .md file or fence for the highlighter, but does not build the block tree (headings, lists, tables, fences as distinct nodes) that a renderer walks to lay out prose versus code versus a table. CodeFence.kt's use of org.intellij.markdown for that full CommonMark AST is Compose rendering plumbing, not something to port as-is; a Rust UI layer will want its own block parser or a crate for it, decided alongside the framework choice in RUST.md.
  • TranscriptUnits.kt (see above) -- deliberately out of scope, since it flattens a row into bounded units for a specific lazy-list framework's composition cost, which is a fact about that framework rather than about the transcript.

Verifying

./run-tests.sh from the repo root now runs event-model, client-core and server in that order (each cargo test, forwarding arguments the same way it always has). From client-core/ directly: cargo test, cargo clippy --all-targets, cargo fmt -- all clean as of this writing.