The App UI is rebuilt on the Cordy Design System: tokens and first-party components replace Material 3 across the production screens.
Added
- Cordy Design System module (
:core:designsystem): colour, type, dimension, and motion tokens plus a component set — button, field, row, text, icon, menu, sheet, dialog, status, top bar, toggle, press indication, haptics, and screen transitions. - Appearance preference in Settings — System, Light, or Dark. The choice is persisted and resolved before the first frame, so a cold start no longer flashes the wrong theme.
- One motion policy gating every screen, which honours the system reduce-motion setting.
- Haptics on commit moments only: send, stop generation, destructive confirm, and error arrival. Ordinary presses, navigation, and sheet opens stay silent.
- Design-law conformance tests covering contrast, shape, dimensions, type, motion, and transitions, plus a law suite per component.
Changed
- Repository navigation now separates current design guidance, curated evidence,
and archived reviews or prototypes. Raw captures and build output live under
ignored
artifacts/paths. - The 28 Maestro flows are grouped into six workflow directories. Their existing tag contract now scans those directories recursively, and documentation links and screenshot destinations follow the new layout.
- The composer remains editable while a reply is streaming. Sending another message cancels the active request and finishes saving the interrupted turn before starting the replacement request.
- Context replay keeps non-empty interrupted replies, omits empty or reasoning-only assistant turns, and trims history by complete user-led turns. The latest input is checked against the system prompt and reserved output budget before submission.
- Chat, settings, and usage screens migrated onto the design system; Material 3 components are gone from those screens. The modal bottom sheet container is the one deliberate exception, kept for its platform drag, scrim, predictive back, and focus-containment behaviour.
- Chat surface: the composer is a flat dock, banners and the conversation library lose their card faces, and status is a glyph instead of a pill.
- Conversation library rows are a single target; rename and delete moved off inline icon buttons onto a long press, with a long-click label so screen readers announce the action set.
- The root screen transition is a crossfade keyed on a coarse phase, so streamed tokens and conversation switches no longer retrigger it.
- Dependencies updated to their latest stable releases: Koog 1.1.1, Ktor 3.5.2, Android Gradle Plugin 9.3.1, AppCompat 1.8.0. Koog 1.1.1 publishes its Android artifacts at minSdk 23, so the manifest override that kept API 24–34 devices supported against Koog 1.0.0's minSdk 35 baseline is no longer needed and has been removed.
- CI runs the whole shared test suite instead of a hand-written module list. The old list still named a module deleted with Cordy Remote, so every push had failed since 2026-08-10, and it silently skipped five modules that do have tests.
- Reasoning traces render through the same pipeline as the answer. A thinking block used to be one raw text node re-laid out on every 40 ms publish; it now goes through the answer's reveal engine — the frame-clock pacer, the jitter buffer, the append-only incremental markdown parse, the fold-surviving ledger and the synchronous settled parse — at caption scale with a flattened heading ladder. The header reads "Thinking" while reasoning arrives and "Thought for Ns" afterwards; the duration is stored on the block, so history keeps it. The trace collapses the moment the answer begins and stays wherever the reader last put it across scrolling and the fold. The 2 dp rule is painted instead of laid out, which removes a second layout pass per publish.
- Settled markdown and code highlighting are cached above the message list and pre-warmed off the main thread for the newest turns, so a row scrolling back into view no longer re-parses its markdown or re-tokenises its code fences. The renderer still draws synchronously from an already-parsed state — no placeholder frame can appear. Syntax highlighting caps at 100 lines instead of 200, since its cost grows super-linearly with length. The list keeps a window of rows around the viewport instead of disposing everything that leaves it, and rows that cannot be edited no longer carry an edit-transition wrapper.
- The reveal ledger holds one account per block of the live draft and owns the frame loop; rows only read it. Stream keys are by block kind rather than by block index.
- The E2E fake runtime can emit reasoning deltas (
cordy.e2e.thinking), its long reply carries code fences, and two new Maestro flows cover heavy-history scrolling and the thinking reveal. A repo-wide scan (CordyMotionLawTest) now enforces the no-infinite-animation law. - The AI disclaimer under the composer shows only while the conversation is empty. It leaves the moment the first message lands and stays gone for that conversation; a new chat shows it again, and a conversation opened from the library never does. It was a permanent footer, which put a legal caveat under every reply the reader scrolled past.
- The message input takes focus and raises the keyboard by itself on a cold start that lands on the empty first conversation — once per launch, and never under the first-run sheet, a drawer, or any other overlay.
Fixed
The jump-to-bottom button now follows actual message content below the viewport, so trailing layout space no longer makes it appear on short replies.
Drafts clear only after the matching submission is accepted. Rejected sends and late acknowledgements preserve text and attachments being edited.
Fast scrolling through a long history janks less. Every settled row used to re-parse its markdown and re-tokenise its code fences on each scroll re-mount, on the main thread. Measured on the Pixel_9 AVD (debug build, cold boot, HEAD and this tree interleaved over a byte-identical 18 × 40-paragraph history): frames over the 16.6 ms budget fell from 10.9% to 2.1% (Perfetto frame timeline),
gfxinfojank from 8.0% to 6.0%, and the 99th-percentile frame from 44.7 ms to 38.0 ms. Layout and draw of large text rows remain the floor on this emulator; seedocs/testing/performance-baseline-2026-08-13.md.The streaming answer re-ran its whole markdown subtree on every publish. The markdown renderer's default
imageTransformeris a fresh instance per composition, provided into a static composition local, which disables Compose skipping for everything beneath it — every already-stable paragraph rebuilt its spans ~25 times a second. One hoisted instance is passed at every call site, and a source-contract test pins it.A paced reveal could strand when the settling row was scrolled away. The frame loop lived inside the row, so a fling during the ~0.9 s settle window cancelled it with the reveal unfinished: the placement spring stayed frozen for the rest of the session and the tail's action row never appeared. The loop now lives above the list, so release does not depend on which rows are mounted.
The pacer's first frame could reveal a whole word up to the network cut point and then pause; it is bounded by the frame's jitter-buffer cap like every other frame.
A reasoning block arriving after the answer had started restarted the answer's reveal from zero.
An expanded reasoning trace vanished in one unsprung frame at the fold, and a trace the reader had opened by hand closed again on every scroll re-use.
OpenAI-compatible reasoning chunks were parsed twice per token; a Responses-only model is now refused with a clear error instead of a 404.
Every OpenAI-compatible provider now reaches its endpoint. Chat requests were sent to a doubled version segment —
…/api/v1/v1/chat/completions— and answered with 404, so OpenRouter, OpenAI, Vercel AI Gateway and Ark were all unusable for chat while their model lists still loaded and made the setup look healthy. Only the Anthropic native channel worked. A CordybaseUrlis now documented and treated everywhere as the API root including its version segment, and the endpoint path appended to it carries none.A truncated reply is no longer stored as a finished one. The OpenAI-compatible adapter treated the end of the response stream as the end of the answer, so a reply cut short by a proxy timeout or a dropped connection was saved as complete — no failure state, no retry, and no way for the reader to know the answer was missing its ending. Completion is now taken from the server's own finish signal.
First run can select a model again. Choosing a provider before entering an API key wiped the built-in model list and reported success, leaving a new user with an empty picker, no explanation, and no way forward. The built-in list now survives, and a run that fetches nothing says so.
An image shared into a cold-started app is no longer dropped. A share arriving while the chat was still loading fell between the two components that each assumed the other had it. It is now held and staged in the composer as soon as the chat is ready.
A failure's banner no longer outlives the answer that replaced it. Regenerate, edit and retry left the previous error on screen, so a successful reply could appear beneath "this conversation is too long, start a new chat".
Two messages created in the same millisecond could be stored out of order, which let editing a question delete an earlier answer. New turns are now ordered strictly after everything already in the conversation.
Accessibility: the
userBubble/onUserBubblecolour pair measured 4.23:1 in light, below the WCAG AA 4.5:1 bar. It was retired along with the filled user bubble that was its only consumer — the user turn is now an accent rule with its text insecondaryLabelon the screen background, a pair that already clears AA in both appearances.