docs: comprehensive CLAUDE.md update for 2026-06-21/22 session
Added 16 new feature commits to the shipped features table. Updated TODO lists (conversation management, biometric unlock, sources all marked done). Documented 10 bugs fixed. Added 3 new constraints/gotchas (keyboard handling, Room schema, BiometricPrompt context). Updated dependencies (biometric), test descriptions (reasoning/sources round-trips, SearchSource converter).
This commit is contained in:
parent
e9e2a66b7d
commit
f8e1747df4
1 changed files with 39 additions and 7 deletions
46
CLAUDE.md
46
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()` |
|
| `cad445f` | **i18n cleanup** — replaced all remaining hardcoded German strings with `stringResource()` |
|
||||||
| `5c54b9b` | **ToolEventsBlock rewrite** — state machine matching web frontend (thinking/analyzing/start/end/error) |
|
| `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 `*/*` |
|
| `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):**
|
**Earlier UI/feel work (Phase 0 prototype → feel-first):**
|
||||||
- Liquid glass styling, floating panels, glassmorphic sidebar
|
- 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`.
|
- **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.
|
- **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)
|
## What's NOT done yet (app, priority order)
|
||||||
|
|
||||||
### 1. Remaining visual/UX gaps (daily-drive blockers)
|
### 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
|
### 2. Conversation management from the app
|
||||||
|
|
||||||
- [ ] **Conversation rename/delete** from the sidebar (currently read-only)
|
- [x] **Conversation rename/delete** from the sidebar — 3-dot menu on each chat, `PATCH`/`DELETE /api/v1/conversations/[id]`
|
||||||
- [ ] **Conversation pin/unpin** from the sidebar
|
- [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
|
- [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
|
### 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] **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] **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
|
- [x] **Tool call display** — `ToolEventsBlock`, parses `\x03` sentinel, status icons per step
|
||||||
- [ ] **Native passkey login** (WebAuthn/FIDO2 on Android)
|
- [ ] **Native passkey login** (WebAuthn/FIDO2 on Android)
|
||||||
- [ ] **WebSocket streaming** (replace chunked HTTP for lower latency on mobile)
|
- [ ] **WebSocket streaming** (replace chunked HTTP for lower latency on mobile)
|
||||||
|
|
@ -303,6 +331,7 @@ composeBom = "2026.02.01"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
kotlinxSerialization = "1.7.3"
|
kotlinxSerialization = "1.7.3"
|
||||||
securityCrypto = "1.1.0-alpha06"
|
securityCrypto = "1.1.0-alpha06"
|
||||||
|
biometric = "1.4.0-alpha02"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
@ -310,10 +339,10 @@ securityCrypto = "1.1.0-alpha06"
|
||||||
**Unit tests** (`app/src/test/`): JVM-only, no emulator needed.
|
**Unit tests** (`app/src/test/`): JVM-only, no emulator needed.
|
||||||
|
|
||||||
- `SettingsViewModelTest` — state transitions, all 4 themes, appearance modes, name updates with store-less default
|
- `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)
|
- `OklabTest` — round-trip conversion, interpolation midpoint, gradient generation (all green since Double-precision fix)
|
||||||
- `MappersTest` — `ConversationSummary` ↔ `ConversationEntity`, `StoredMessage` ↔ `MessageEntity`, round-trip integrity, attachment preservation
|
- `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<Attachment>` ↔ JSON round-trips, malformed JSON returns empty list, unknown fields tolerance via `ignoreUnknownKeys`
|
- `ConvertersTest` — `List<Attachment>` + `List<SearchSource>` ↔ 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
|
- `ColorPipelineTest` — all tokens in DisplayP3, approximate value ranges, no pure black/white in neutrals
|
||||||
- `SquircleShapeTest` — KaizenShapes token existence/distinctness, SquircleShape toString
|
- `SquircleShapeTest` — KaizenShapes token existence/distinctness, SquircleShape toString
|
||||||
- `ThemeRegistryTest` — 4 themes present, forId lookup, fromString parsing + fallback, distinct blob colors
|
- `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.
|
- **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`.
|
- 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`.
|
- **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.
|
- **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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue