docs: update CLAUDE.md with all session changes

Added: gradient scrims, mode pills multi-select, EmptyHero centering,
settings layout fix, theme switching wiring, i18n cleanup, ToolEventsBlock
state machine, attachment picker menu, background prefetch, migration
watermark fix documentation.
This commit is contained in:
Bruno Deanoz 2026-06-21 18:48:52 +02:00
parent 09489318bf
commit 2e323f3609

View file

@ -78,6 +78,10 @@ Full spec: `DESIGN.md`.
**Migration status:** All UI components migrated to the design system. Sidebar, ChatInput, ChatScreen, LoginScreen, ModelSheet, SettingsCard, SettingsScreen use `GlassSurface` + `KaizenShapes` + `kaizenShadow`. Only Markdown code-block shapes remain as `RoundedCornerShape` (intentionally rectangular for code). **Migration status:** All UI components migrated to the design system. Sidebar, ChatInput, ChatScreen, LoginScreen, ModelSheet, SettingsCard, SettingsScreen use `GlassSurface` + `KaizenShapes` + `kaizenShadow`. Only Markdown code-block shapes remain as `RoundedCornerShape` (intentionally rectangular for code).
**Theme switching fully wired:** All 7 files that previously imported hardcoded `Amber`/`AmberDeep`/`OnAmber`/`BlobX` now read from `LocalKaizenAccent.current`. Changing theme in Settings immediately updates: message bubbles, send button, orb, mesh background blobs, sidebar highlights, model sheet, stream blocks, login screen, settings toggles. No hardcoded amber imports remain outside `Color.kt` and theme definitions.
**Floating bar scrims:** Top bar (hamburger + model pill) and bottom dock (mode pills + input) have vertical gradient scrims (`MaterialTheme.colorScheme.background` → transparent) to prevent message content from bleeding through. Model pill opacity increased to 0.92.
**KaizenOrb** (`chat/Background.kt`) uses `rememberTiltState()` from `ui/sensor/` instead of inline sensor code. Breath animation uses `Durations.ORB_BREATH` / `ORB_BREATH_STREAMING` from `ui/motion/`. **KaizenOrb** (`chat/Background.kt`) uses `rememberTiltState()` from `ui/sensor/` instead of inline sensor code. Breath animation uses `Durations.ORB_BREATH` / `ORB_BREATH_STREAMING` from `ui/motion/`.
**Typography** — Inter Variable (`res/font/inter_variable.ttf`, 859 KB, SIL OFL). Full type scale from DESIGN.md §8: `displayLarge` (32sp/W600) through `labelSmall` (11sp/W500). All 9 weight axes registered (W100W900). Negative tracking at display sizes, positive at label sizes. `@OptIn(ExperimentalTextApi::class)` for `FontVariation.Settings`. **Typography** — Inter Variable (`res/font/inter_variable.ttf`, 859 KB, SIL OFL). Full type scale from DESIGN.md §8: `displayLarge` (32sp/W600) through `labelSmall` (11sp/W500). All 9 weight axes registered (W100W900). Negative tracking at display sizes, positive at label sizes. `@OptIn(ExperimentalTextApi::class)` for `FontVariation.Settings`.
@ -112,7 +116,7 @@ The `/api/v1/chat` stream embeds control sections via sentinel code points (from
| `\x00` USAGE | trailing | Usage/cost JSON | Stripped (display TODO) | | `\x00` USAGE | trailing | Usage/cost JSON | Stripped (display TODO) |
| `\x01` REASONING | toggle | Model's chain-of-thought | `ReasoningBlock` — collapsible "Gedankengang" | | `\x01` REASONING | toggle | Model's chain-of-thought | `ReasoningBlock` — collapsible "Gedankengang" |
| `\x02` SOURCES | trailing | Web search sources JSON | `SourcesBlock` — compact cards with domain | | `\x02` SOURCES | trailing | Web search sources JSON | `SourcesBlock` — compact cards with domain |
| `\x03` TOOL | leading | Tool-call events (JSON+newline) | `ToolEventsBlock` — status icons per step | | `\x03` TOOL | leading | Tool-call events (JSON+newline) | `ToolEventsBlock` — state machine (thinking/analyzing/start/end/error) |
| `\x04` QUERY | trailing | Search query string | Shown above sources | | `\x04` QUERY | trailing | Search query string | Shown above sources |
**StreamConsumer.Incremental** (`net/StreamConsumer.kt`) returns `StreamState` (not `String`): **StreamConsumer.Incremental** (`net/StreamConsumer.kt`) returns `StreamState` (not `String`):
@ -126,6 +130,14 @@ The `/api/v1/chat` stream embeds control sections via sentinel code points (from
**UI files:** `chat/StreamBlocks.kt``ReasoningBlock`, `ToolEventsBlock`, `SourcesBlock`, `SourceChip`. **UI files:** `chat/StreamBlocks.kt``ReasoningBlock`, `ToolEventsBlock`, `SourcesBlock`, `SourceChip`.
**ToolEventsBlock state machine** (matches web frontend `LiveToolSteps`):
- `type: "thinking"` → "✨ Verarbeitet …" (only shown when no other tool activity)
- `type: "analyzing"` → "📄 Analysiert {name} …" (file processing)
- `type: "start"` → wrench icon + tool name (in progress)
- `type: "end"` → ✅ checkmark + tool name + elapsed ms (completed)
- `type: "error"` → ❌ error icon + tool name (failed)
- Empty/thinking-only events are hidden (no generic "Tool" fallback).
### App icon ### App icon
Adaptive icon: amber orb with multi-layer radial gradients (body, depth shadow, specular highlight, warm bounce light, crisp rim). Light mode: warm off-white background. Dark mode: Obsidian gradient. Monochrome layer for Android 13+ themed icons. Splash screen uses orb foreground (API 31+). Vector-only — no raster WebPs. Adaptive icon: amber orb with multi-layer radial gradients (body, depth shadow, specular highlight, warm bounce light, crisp rim). Light mode: warm off-white background. Dark mode: Obsidian gradient. Monochrome layer for Android 13+ themed icons. Splash screen uses orb foreground (API 31+). Vector-only — no raster WebPs.
@ -146,19 +158,22 @@ Architecture decision document: `OFFLINE_CACHE_ADD.md`.
``` ```
App start → Room (instant) → UI shows cached data App start → Room (instant) → UI shows cached data
→ Background: server fetch → Room update → Flow emits → UI updates → Background: server fetch → Room update → Flow emits → UI updates
→ Background: prefetch messages for all uncached conversations
Chat open → Room (instant) → Background: server fetch → Room update Chat open → Room (instant) → Background: server fetch → Room update
Send message → in-memory during streaming → Room flush after stream ends Send message → in-memory during streaming → Room flush after stream ends
``` ```
**Background prefetch:** After conversation list sync, `MessageRepository.prefetchMissing()` iterates all unlocked conversations and fetches messages for any not yet cached. This ensures all chats are readable offline, not just previously opened ones. Already-cached conversations are skipped.
**Streaming hybrid model:** During active streaming, messages are mutated in-memory (60x/s). Room is NOT written during streaming — that would be absurd I/O. After stream completion, user + assistant messages are upserted into Room. This avoids SQLite lock contention while keeping the cache fresh. **Streaming hybrid model:** During active streaming, messages are mutated in-memory (60x/s). Room is NOT written during streaming — that would be absurd I/O. After stream completion, user + assistant messages are upserted into Room. This avoids SQLite lock contention while keeping the cache fresh.
**Files:** **Files:**
- `db/Entities.kt``ConversationEntity`, `MessageEntity` - `db/Entities.kt``ConversationEntity`, `MessageEntity`
- `db/ConversationDao.kt``observeAll()` (Flow), `replaceAll()` (transaction: deleteAll + upsertAll, avoids SQLite 999-variable limit), `lastCachedAt()` - `db/ConversationDao.kt``observeAll()` (Flow), `replaceAll()` (transaction: deleteAll + upsertAll, avoids SQLite 999-variable limit), `lastCachedAt()`, `allUnlockedIds()`
- `db/MessageDao.kt``observeByConversation()`, `getByConversation()`, `upsertAll()`, `deleteByConversation()` - `db/MessageDao.kt``observeByConversation()`, `getByConversation()`, `upsertAll()`, `deleteByConversation()`, `cachedConversationIds()`
- `db/KaizenDatabase.kt` — singleton via `companion object` (double-checked locking), thread-safe - `db/KaizenDatabase.kt` — singleton via `companion object` (double-checked locking), thread-safe
- `db/ConversationRepository.kt``observeAll()` (Flow of ConversationSummary), `refresh()` (server → Room) - `db/ConversationRepository.kt``observeAll()` (Flow of ConversationSummary), `refresh()` (server → Room), `allUnlockedIds()`
- `db/MessageRepository.kt``observeMessages()`, `getCached()`, `fetchAndCache()`, `cacheMessages()` - `db/MessageRepository.kt``observeMessages()`, `getCached()`, `fetchAndCache()`, `cacheMessages()`, `prefetchMissing()` (background prefetch for offline)
- `db/Converters.kt``List<Attachment>` ↔ JSON string via kotlinx.serialization - `db/Converters.kt``List<Attachment>` ↔ JSON string via kotlinx.serialization
- `db/Mappers.kt``ConversationSummary``ConversationEntity`, `StoredMessage``MessageEntity` - `db/Mappers.kt``ConversationSummary``ConversationEntity`, `StoredMessage``MessageEntity`
@ -197,6 +212,16 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
| `4616ef2` | **Markdown rendering with syntax-highlighted code blocks** — native Compose renderer: headings, bold/italic, inline code, fenced code blocks with keyword highlighting (15+ languages), ordered/unordered lists, streaming cursor integration | | `4616ef2` | **Markdown rendering with syntax-highlighted code blocks** — native Compose renderer: headings, bold/italic, inline code, fenced code blocks with keyword highlighting (15+ languages), ordered/unordered lists, streaming cursor integration |
| `d8dcb25` | **Attachment support — display, upload, and send files** — parse + render message attachments from loaded conversations (images inline via OkHttp + in-memory cache, other kinds as icon chips); + button wired to Android file picker with upload to `/api/v1/upload`, progress chips, send attachments with chat requests + persist in saved messages. Also: Markdown horizontal rules, blockquotes, links, code block copy button | | `d8dcb25` | **Attachment support — display, upload, and send files** — parse + render message attachments from loaded conversations (images inline via OkHttp + in-memory cache, other kinds as icon chips); + button wired to Android file picker with upload to `/api/v1/upload`, progress chips, send attachments with chat requests + persist in saved messages. Also: Markdown horizontal rules, blockquotes, links, code block copy button |
| `9759d47` | **Offline-first cache layer (Room)** — sidebar + messages load instantly from SQLite cache, background refresh syncs with server, streaming hybrid model (in-memory during stream, Room flush after), ChatViewModel, repositories, mappers, TypeConverters, unit tests | | `9759d47` | **Offline-first cache layer (Room)** — sidebar + messages load instantly from SQLite cache, background refresh syncs with server, streaming hybrid model (in-memory during stream, Room flush after), ChatViewModel, repositories, mappers, TypeConverters, unit tests |
| `5804b33` | **Gradient scrims on floating top/bottom bars** — content no longer bleeds through the model pill or mode pills |
| `78f03ad` | **Mode pills cleanup** — removed muddy amber tint, solid surface fills, neutral borders |
| `3560665` | **Mode pills multi-select** — modes are now independent toggles, not mutually exclusive |
| `e4c6e7f` | **EmptyHero centered** — orb + greeting vertically centered between top/bottom bars |
| `fe5e93c` | **Settings layout fix** — option rows stack title above controls instead of cramming side-by-side |
| `9186594` | **Background prefetch** — all conversation messages prefetched for offline access on app start |
| `d468ec8` | **Theme switching wired** — all 7 files migrated from hardcoded Amber to `LocalKaizenAccent.current` |
| `cad445f` | **i18n cleanup** — replaced all remaining hardcoded German strings with `stringResource()` |
| `5c54b9b` | **ToolEventsBlock rewrite** — state machine matching web frontend (thinking/analyzing/start/end/error) |
| `0948931` | **Attachment picker menu** — camera capture, gallery (image/*), and file picker (*/*) via dropdown |
**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
@ -207,9 +232,11 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
- Dynamic keyboard handling (suggestion pills hide, spacer shrinks) - Dynamic keyboard handling (suggestion pills hide, spacer shrinks)
- Settings screen with SettingsViewModel - Settings screen with SettingsViewModel
### Bug fixed this session (2026-06-19/20) ### Bugs fixed (2026-06-19/20/21)
**"Verbindung zum Server fehlgeschlagen" on S25** — root cause was **server running an old build** without the v1/conversations route (404) and without Bearer auth on models (401). Fix was `git pull && docker compose -f docker-compose.prod.yml up -d --build` on the server. Additionally, the error diagnostics were improved (see `423abc0`) so future issues show the exact cause in the UI banner instead of a generic message. - **"Verbindung zum Server fehlgeschlagen" on S25** (06-19) — server running old build without v1 routes. Fix: `git pull && docker compose up -d --build`.
- **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 00170025 (60-second increments) can cause watermark conflicts. Future migrations via `pnpm db:generate` use real timestamps and won't hit this.
## What's NOT done yet (app, priority order) ## What's NOT done yet (app, priority order)
@ -238,7 +265,7 @@ These are what separate "prototype" from "daily-driver":
### 4. Features for parity with the web ### 4. Features for parity with the web
- [x] **Attachments** — image/file upload, attachment preview in messages, display in loaded conversations (camera capture still TODO) - [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` + `SourceChip`, parses `\x02`/`\x04` sentinels
- [x] **Tool call display**`ToolEventsBlock`, parses `\x03` sentinel, status icons per step - [x] **Tool call display**`ToolEventsBlock`, parses `\x03` sentinel, status icons per step