diff --git a/CLIENT_CORE.md b/CLIENT_CORE.md new file mode 100644 index 0000000..2f98138 --- /dev/null +++ b/CLIENT_CORE.md @@ -0,0 +1,130 @@ +# 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 | +| *(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, 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` 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. + +## 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. diff --git a/run-tests.sh b/run-tests.sh index de20d4d..58f7f01 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,11 +1,18 @@ #!/bin/sh -# Runs this repo's tests. Extra arguments are forwarded to `cargo test`, -# e.g. `./run-tests.sh transcript` to run just the transcript tests. +# Runs this repo's tests. Extra arguments are forwarded to each `cargo test`, +# e.g. `./run-tests.sh transcript` to run just the transcript tests in all +# three crates. # -# Only `server/` has tests: it holds all the logic worth testing (event -# normalization, transcript cursors, config persistence, token auth), while -# the Android app is UI over its HTTP API. Verifying the app means running -# it -- see AGENTS.md. +# server/ holds the backend's own logic (event normalization, transcript +# cursors, config persistence, token auth); event-model is the wire shape +# server/ and client-core share; client-core is the app's pure logic held +# once instead of twice (see CLIENT_CORE.md) -- the syntax highlighter, the +# ANSI parser, the transcript cache and fold, the REST and SSE clients. It +# is not wired into the Android app yet (RUST.md), which still carries its +# own Kotlin copies and has no tests of its own to run here; verifying it +# means running it -- see AGENTS.md. set -eu -cd "$(dirname "$0")/server" -exec cargo test "$@" +cd "$(dirname "$0")" +for crate in event-model client-core server; do + (cd "$crate" && cargo test "$@") +done