diff --git a/CLAUDE.md b/CLAUDE.md index 8289a0e..b451897 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,6 +224,21 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo | `cad445f` | **i18n cleanup** — replaced all remaining hardcoded German strings with `stringResource()` | | `5c54b9b` | **ToolEventsBlock rewrite** — state machine matching web frontend (thinking/analyzing/start/end/error) | | `da01b57` | **Attachment picker menu** — three floating GlassSurface pills (camera/gallery/file) with colored round icons, slide animation, dismiss-on-tap-outside. Positioned bottom-left above the + button. Camera uses `TakePicturePreview`, gallery filters `image/*`, file allows `*/*` | +| `f3d8154` | **Sidebar redesign** — GlassSurface opacity 0.92/0.88, flat conversation rows, 3-dot context menu (rename/delete/pin/lock via `PATCH`/`DELETE /api/v1/conversations`), smaller avatar (30dp), logout integrated into user card | +| `f3d8154` | **Markdown typography overhaul** — line-height 23→26sp (×1.625), paragraph spacing 6→12dp, heading margins 18dp/8dp, list spacing doubled. Matches Gemini readability | +| `532cb8c` | **Reasoning + sources persistence** — saved to server (`SaveMessage.reasoning/sources`), loaded back (`StoredMessage`), cached in Room (`MessageEntity` + `SearchSource` TypeConverter). Reasoning rendered above content, sources below | +| `88559bf` | **Biometric unlock for locked conversations** — Android KeyStore AES key with `setUserAuthenticationRequired(true)` + CryptoObject, hardware-enforced. Server unlock token via `POST /api/v1/auth/unlock` + `X-Unlock-Token` header. Auto-lock on app background | +| `30d3f28` | **Search mode functional** — `webSearch: true` in ChatRequest triggers agentive web search. Sources/query parsed by StreamConsumer, rendered in collapsible SourcesBlock with clickable links (ACTION_VIEW) | +| `f6dc5db` | **Reasoning presets + sampling controls** — 8-preset dropdown (Sofort→Maximal) sends `reasoningEffort`/`preferThroughput`. Sampling popover with per-parameter toggle + slider for Temperature/Top-P/Top-K | +| `3ca1d2c` | **Token counter** — parses usage sentinel from stream (`inputTokens`/`outputTokens`), shows ⚡ badge in top bar. Sums tokens from server messages for loaded chats. Context length from model's `context_length` field | +| `3ca1d2c` | **3D user bubble** — multi-layer gradient, inner highlight, kaizenShadow level1, thicker border | +| `3ca1d2c` | **Scroll buttons** — glass arrow at center-right, directional (up OR down), hidden during streaming | +| `f784902` | **Orb rewrite** — transparent glass lens (15-22% alpha tint) instead of opaque painted ball. Background blobs bleed through. Elliptical speculars, double rim, subtle caustics | +| `4ebe812` | **Theme visibility fix** — added missing blob3, increased blob alpha (0.30-0.42), reduced vignette (0.40). Themes now visually distinct | +| `f22838e` | **Message actions** — Copy/Delete/Regenerate buttons under each message. Delete via `DELETE /api/v1/conversations/[id]/messages` | +| `2d57aff` | **Input field redesign** — pill shape, higher opacity (0.85/0.90), Mic/Send toggle (mic when empty, send when text), bottom-aligned buttons | +| `e9e2a66` | **Stronger haptics** — send CLICK 1.0, thinkingStart double-tap, responseStart crescendo, responseEnd firm settle | +| `e9e2a66` | **Live language switching** — `LocaleManager` (API 33+) or `recreate()` fallback, immediate effect | **Earlier UI/feel work (Phase 0 prototype → feel-first):** - Liquid glass styling, floating panels, glassmorphic sidebar @@ -240,6 +255,19 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo - **Server 500 on conversation load** (06-21) — Drizzle migration 0025 (`model_id` column) skipped due to rogue watermark entry (`created_at = 1781600000000`) in `drizzle.__drizzle_migrations` table, caused by hand-typed fake timestamps. Fix: lowered rogue entry's timestamp, re-ran `node scripts/migrate.mjs`. - **Deploy gotcha (migrations):** fake timestamps in journal entries 0017–0025 (60-second increments) can cause watermark conflicts. Future migrations via `pnpm db:generate` use real timestamps and won't hit this. +### Bugs fixed (2026-06-21/22 — app session) + +- **Room crash on launch** — schema hash mismatch after adding `reasoning`/`sources` fields to `MessageEntity` with version still at 1. Fix: bump to version 2, `fallbackToDestructiveMigration(true)`. +- **Keyboard handling** — `imePadding()` on Compose Box/Column caused input field to float to top of screen OR be hidden behind keyboard. Fix: read `WindowInsets.ime.getBottom()` directly, apply as negative Y `offset()` on the bottom dock. `windowSoftInputMode=adjustNothing` in manifest. +- **Locked chats not opening** — `context as? FragmentActivity` silently returned null (ContextWrapper). Fix: walk ContextWrapper chain. Added fallback when biometrics unavailable. +- **Error banner stuck** — `loadError` never cleared after unlock failure. Fix: auto-clear after 4s, reset on navigation. +- **Reasoning/sources lost** — not persisted to server or Room cache. Fix: added fields to `SaveMessage`, `StoredMessage`, `MessageEntity`, `Converters`, `Mappers`. +- **Reasoning at bottom** — rendered below content like sources. Fix: moved above content in `ChatComponents`. +- **Themes barely visible** — blob3 never rendered, alpha too low (0.22-0.30), vignette too aggressive (0.55). Fix: 4 blobs, alpha 0.30-0.42, vignette 0.40. +- **Mode pills were mock** — didn't send anything to backend. Fix: search sends `webSearch: true`, reasoning sends `reasoningEffort`/`preferThroughput`, sampling sends `temperature`/`topP`/`topK`. +- **Token counter always 0** — only updated during streaming, not for loaded chats. Fix: sum `inputTokens + outputTokens` from server messages. +- **Model pill wrong** — showed global default instead of per-chat model. Fix: `chatModel` state from last assistant message's `model` field. + ## What's NOT done yet (app, priority order) ### 1. Remaining visual/UX gaps (daily-drive blockers) @@ -254,9 +282,9 @@ These are what separate "prototype" from "daily-driver": ### 2. Conversation management from the app -- [ ] **Conversation rename/delete** from the sidebar (currently read-only) -- [ ] **Conversation pin/unpin** from the sidebar -- [x] **In-app unlock for locked conversations** — BiometricPrompt with CryptoObject (AES key in TEE/StrongBox), hardware-enforced biometric auth → server unlock token via `POST /api/v1/auth/unlock` → fetch messages with `X-Unlock-Token` header +- [x] **Conversation rename/delete** from the sidebar — 3-dot menu on each chat, `PATCH`/`DELETE /api/v1/conversations/[id]` +- [x] **Conversation pin/unpin** from the sidebar — `PATCH { pinned }`, star icon for pinned items +- [x] **In-app unlock for locked conversations** — BiometricPrompt with CryptoObject (AES key in TEE/StrongBox), hardware-enforced biometric auth → server unlock token via `POST /api/v1/auth/unlock` → fetch messages with `X-Unlock-Token` header. Auto-lock on background. Fallback to direct unlock if biometrics unavailable ### 3. Security hardening @@ -269,7 +297,7 @@ These are what separate "prototype" from "daily-driver": - [x] **Attachments** — image/file upload, attachment preview in messages, display in loaded conversations, camera capture, attachment picker menu (camera/gallery/file) - [x] **Reasoning/thinking display** — `ReasoningBlock` (collapsible), parses `\x01` sentinel -- [x] **Web search sources** — `SourcesBlock` + `SourceChip`, parses `\x02`/`\x04` sentinels +- [x] **Web search sources** — `SourcesBlock` (collapsible, clickable links via ACTION_VIEW), parses `\x02`/`\x04` sentinels. `webSearch: true` in ChatRequest triggers agentive search - [x] **Tool call display** — `ToolEventsBlock`, parses `\x03` sentinel, status icons per step - [ ] **Native passkey login** (WebAuthn/FIDO2 on Android) - [ ] **WebSocket streaming** (replace chunked HTTP for lower latency on mobile) @@ -303,6 +331,7 @@ composeBom = "2026.02.01" okhttp = "4.12.0" kotlinxSerialization = "1.7.3" securityCrypto = "1.1.0-alpha06" +biometric = "1.4.0-alpha02" ``` ## Tests @@ -310,10 +339,10 @@ securityCrypto = "1.1.0-alpha06" **Unit tests** (`app/src/test/`): JVM-only, no emulator needed. - `SettingsViewModelTest` — state transitions, all 4 themes, appearance modes, name updates with store-less default -- `StreamConsumerTest` — batch + incremental parser, sentinel stripping, reasoning/sources/tools parsing, char-by-char edge cases +- `StreamConsumerTest` — batch + incremental parser, sentinel stripping, reasoning/sources/tools/usage parsing, char-by-char edge cases - `OklabTest` — round-trip conversion, interpolation midpoint, gradient generation (all green since Double-precision fix) -- `MappersTest` — `ConversationSummary` ↔ `ConversationEntity`, `StoredMessage` ↔ `MessageEntity`, round-trip integrity, attachment preservation -- `ConvertersTest` — `List` ↔ JSON round-trips, malformed JSON returns empty list, unknown fields tolerance via `ignoreUnknownKeys` +- `MappersTest` — `ConversationSummary` ↔ `ConversationEntity`, `StoredMessage` ↔ `MessageEntity`, round-trip integrity, attachment preservation, reasoning round-trip, null reasoning preserved, sources round-trip, empty sources → null, reasoning+sources together +- `ConvertersTest` — `List` + `List` ↔ JSON round-trips, malformed JSON returns empty list, unknown fields tolerance via `ignoreUnknownKeys` - `ColorPipelineTest` — all tokens in DisplayP3, approximate value ranges, no pure black/white in neutrals - `SquircleShapeTest` — KaizenShapes token existence/distinctness, SquircleShape toString - `ThemeRegistryTest` — 4 themes present, forId lookup, fromString parsing + fallback, distinct blob colors @@ -328,4 +357,7 @@ securityCrypto = "1.1.0-alpha06" - **Deploy gotcha (learned 2026-06-19):** `git pull` BEFORE `docker compose up --build`, else the image bakes stale code and the app gets 404/401 against endpoints that exist in the repo but aren't in the running build. - Headless compile on Mac: `JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./gradlew :app:compileDebugKotlin` — JDK 21 via Homebrew, Android SDK at `/opt/homebrew/share/android-commandlinetools`. - **AGP 9.x + KSP:** requires `android.disallowKotlinSourceSets=false` in `gradle.properties`. +- **Keyboard handling:** do NOT use `imePadding()` (inflates the dock composable, input floats to top) or `adjustResize` (hides input behind keyboard with edge-to-edge). Instead: read `WindowInsets.ime.getBottom()` and apply as `Modifier.offset(y = -bottomPx.toDp())` on the bottom dock. `windowSoftInputMode=adjustNothing` in manifest. +- **Room schema changes:** bump `version` in `@Database` annotation AND use `fallbackToDestructiveMigration(true)`. `false` only covers downgrades, not same-version hash mismatches → crash. +- **BiometricPrompt context:** `LocalContext.current` may be a `ContextWrapper`, not `FragmentActivity`. Walk the wrapper chain: `while (ctx is ContextWrapper) { if (ctx is FragmentActivity) break; ctx = ctx.baseContext }`. - **Room is a cache:** `fallbackToDestructiveMigration()` means the SQLite DB is wiped on schema changes. No data loss — server is source of truth, cache rebuilds on next fetch.