Pencil icon on user messages (next to copy, before delete). Tap removes
the edited message and all messages after it from the UI + server, then
puts the text back into the input field for editing and resending.
Only shown on user messages with text content (not voice-only), and only
when not currently streaming.
Voice auto-send was saving messages to the server but not caching them
in Room with correct sortOrder. When navigating away and back, the app
loaded from Room cache (empty for voice messages) then from server,
where the tree-based ordering could differ from insertion order.
Now caches both user and assistant messages in Room with proper sortOrder
after the server save succeeds, matching the normal send() flow.
Audio-only user messages (no text, only audio attachments) now render as
a clean "🎤 Sprachnachricht" pill instead of the ugly filename chip
(voice_1782170416740.m4a). Detects via attachments.all { kind == "audio" }
&& content.isBlank(). Regular file attachments are unaffected.
When search mode is active and a vertex: model is selected, a second pill
appears next to "Suche" with a dropdown to choose between Google Search
(default) and Enterprise Web Search grounding. The selection is sent as
webSearchProvider in the chat request body. Matches the web frontend's
OptionPill for webSearchProvider.
Only visible for Vertex models — OpenRouter uses agentive web_search
server-tool which has no provider toggle.
Sampling panel: width 260→220dp, padding reduced, spacing 14→6dp between
parameters, checkbox 20→16dp, slider height capped at 28dp, removed extra
spacer between label row and slider. Panel is now ~40% shorter.
Pills visibility: inactive background alpha raised (dark 0.06→0.10, light
0.04→0.07), border alpha doubled (dark 0.08→0.14, light was white-on-light
invisible, now uses black 0.10), icon tint 0.60→0.75, text 0.70→0.80.
Active state also strengthened. Pills are now clearly visible in both modes.
32-bar waveform visualization that reacts to microphone amplitude in
real-time. MediaRecorder.getMaxAmplitude() polled every 60ms, normalized
to 0-1 range, fed into a scrolling bar chart (new bars enter right, old
shift left). Bars use accent color — louder = taller + more opaque,
quiet = short + muted. Replaces the static "Aufnahme läuft…" text.
Voice messages now upload and send automatically when recording stops —
no manual send button needed. The input area transforms during recording:
text field replaced by a centered pulsing red dot + "Aufnahme läuft…" label,
with a single centered red stop button below.
The old flow (record → attachment chip → manual send) treated voice like
a file attachment. New flow: tap mic → record → tap stop → auto-upload →
auto-send → stream response. Matches the expected voice message UX.
Buttons: 30dp→34dp with squircle shape (KaizenShapes.sm), subtle tinted
background (dark: white 4%, light: black 2.5%), icons 16→17dp. Tint alpha
raised from 0.35→0.50 (copy/regenerate/volume) and 0.6→0.70 (delete) so
buttons are actually visible in both light and dark mode. Spacing 2→4dp
between buttons, top padding 4→6dp.
Items use KaizenShapes.md (16dp radius) instead of sm (12dp), more spacing
(4dp gap), horizontal padding, and subtle glass borders on inactive items.
Active items get accent gradient border + shadow. Matches web's rounded-xl
card treatment.
Design: lower tintAlpha (0.80/0.86) for more blob bleed-through, accent-tinted
gradient borders + level1 shadow on active items, specular borders on search bar
and user card, accent gradient avatar, separator line in context menu.
Features: live search filter, "Titel generieren" (Sparkles, manual POST with
{ manual: true }), "Duplizieren" (copies all messages with new IDs + parentId
chain), titleGenerated field on ConversationSummary/Entity (Room v4).
Audio recording:
- Mic button wired to MediaRecorder (AAC/M4A, 44.1kHz/128kbps)
- Tap to start, tap again to stop — auto-uploads as audio attachment
- RECORD_AUDIO permission with runtime request
- Recording state: red stop-square on mic button
TTS read-aloud:
- Volume button on assistant messages → POST /api/v1/audio-gen
- Gemini-TTS generates speech server-side, MediaPlayer streams it
- Tap again to stop playback, accent-colored icon when playing
- Graceful error handling (Toast on failure)
Supporting changes:
- KaizenApi.generateSpeech() for /api/v1/audio-gen endpoint
- KaizenIcons.Volume2 added (Lucide)
- DE/EN strings for recording, permission, TTS states
The two-row ChatInput redesign made the bottom dock ~210dp tall but
contentPadding was still 152dp, causing the last message to scroll
behind the mode pills.
- Mark auto-logout on 401 as done
- Remove WebSocket text-streaming TODO (not needed)
- Add Voice & Call Mode section with v1 (pipeline) and v2 (Gemini Live API via Vertex/GCP)
- Add Image & media generation section
- Document stop button, locale fix, login shadow migration in shipped features
- EmptyHero date respects system locale instead of hardcoded German
- Stop button (red square) replaces send/call during streaming, cancels stream cleanly
- Auto-logout on HTTP 401 (expired token) routes to login instead of error banner
- LoginScreen migrated from Modifier.shadow to kaizenShadow (design system pillar 4)
Same 38dp circle as SendButton. Subtle accent gradient fill (16-28%
alpha), AudioLines icon in accent.primary color, press-scale animation.
Visually pairs with the send button as its idle counterpart.
Mic (voice recording) stays left next to +. Right side toggles
between AudioLines icon (call, when empty) and send button
(when content). Send button: removed border, translucent gradient,
38dp instead of 44dp.
Sensor: SENSOR_DELAY_GAME → SENSOR_DELAY_UI (~15Hz instead of ~60Hz),
parallax effect unchanged. Blob drift animation only runs on empty
state, frozen at midpoint during chat (blobs still visible, no GPU
animation cost).
Text field on top with full width, action icons below.
More padding (16dp margins, 14dp inner), pill shape with
32dp corners, level3 shadow for float effect. Plus and Mic
buttons get subtle circular backgrounds.
Gallery uses PickMultipleVisualMedia (image + video), file picker uses
OpenMultipleDocuments — both now allow selecting multiple items at once.
Camera stays single-shot (one photo per shutter, standard behavior).
Each selected item uploads in parallel as before.
Icon system is now the 7th pillar alongside P3 color, squircle shapes,
GlassSurface, multi-layer shadows, 4-theme system, and sensor motion.
Documents the Lucide base, custom override architecture, semantic
grouping, and how to add hand-drawn SVG overrides. Also documents the
unified top bar redesign (merged sidebar + model pill).
Introduces a centralized icon registry (ui/icon/KaizenIcons.kt) that
decouples every UI component from the underlying icon library. All 10
source files migrated from Material Icons Rounded to KaizenIcons backed
by Lucide (1.5px stroke, same library as the web frontend).
Architecture:
- KaizenIcons object with semantic getters (Send, Lock, Brain, etc.)
- Grouped by function (Navigation, Chat, Modes, Files, Settings, etc.)
- Custom brand section ready for hand-drawn SVG overrides
- One-line swap: change the getter, every usage updates
- Zero Material Icon imports remaining in app source
Dependency: com.composables:icons-lucide-android:1.0.0
Three fixes for sluggish conversation list:
1. Optimistic sidebar insert: new conversation appears in the sidebar
immediately after creation (with first-line preview as title),
before the stream finishes.
2. generateTitle now returns the title from the server response and
updates Room directly via updateTitle() — no blind refresh that
races with server-side generation.
3. replaceAll uses upsert + deleteExcept instead of deleteAll + insert,
eliminating the empty-list flash that caused sidebar flicker.
Merge the separate hamburger circle and ModelPill into one cohesive
GlassSurface pill. Menu icon (17dp, muted) sits left, subtle 3dp dot
divider, then model name (13sp Medium, 180dp max with ellipsis).
No more chunky ExpandMore chevron. Matches the TokenBadge's glass
language on the right.
Security section in Settings with three cards:
- Biometric toggle (persisted in SecureStore, respected by unlock flow)
- Signed-in devices list with remote revoke (GET/DELETE /api/v1/auth/tokens)
- Password change form (POST /api/v1/auth/change-password)
requestBiometricOrPassword() now checks the biometric toggle — when
disabled, always falls back to password dialog.
Three paths in openConversation() called unlockAndOpen() without any
authentication when biometrics were unavailable. Sidebar ToggleLock
also sent PATCH { locked: false } without auth.
- Unified requestBiometricOrPassword() gates all unlock paths
- PasswordUnlockDialog as fallback when biometrics unavailable
- KaizenApi.requestUnlock() sends password to server for verification
- All unlock flows (open chat + sidebar toggle) now require auth
1. Mode pills stay visible when keyboard open (were hidden).
Spacing reduced (6dp vs 24dp) to keep dock compact.
2. Token counter works for loaded chats — sum inputTokens +
outputTokens from all StoredMessages when fetching from
server. StoredMessage now parses model/inputTokens/outputTokens.
3. Model pill shows the chat's actual model (from last assistant
message) instead of always the global default. chatModel state
tracks per-conversation model, falls back to session default
for new chats.
adjustResize didn't work with enableEdgeToEdge (input hidden
behind keyboard). imePadding inflated the dock (input too high).
New approach: read WindowInsets.ime bottom value directly and
apply it as negative Y offset on the dock. The dock slides up
by exactly the keyboard height — no inflation, no layout changes.
windowSoftInputMode=adjustNothing (edge-to-edge default).
When keyboard closed: offset = nav bar height (gesture bar).
When keyboard open: offset = keyboard height (includes nav bar).
imePadding() on Compose Box/Column with overlapping children
(BottomCenter-aligned dock over fillMaxSize LazyColumn) caused
the input to float to the top of the screen. The Compose inset
system doesn't work well with this overlay layout pattern.
Fix: remove all imePadding() usage, set windowSoftInputMode=
adjustResize in AndroidManifest. Android resizes the window
itself when the keyboard opens — the entire layout naturally
reflows, dock stays at the bottom, chat area shrinks. This is
the standard approach for chat apps with edge-to-edge.
- loadError auto-clears after 4s for unlock failures
- loadError reset on every conversation switch (was sticky)
- Token badge always shown (was hidden when usedTokens == 0,
making it invisible for all loaded chats)
Note: locked chat "Authentifizierungsfehler" happens because
POST /api/v1/auth/unlock is not deployed yet on the server.
After git pull + docker build on prod, biometric unlock works.
The root cause: imePadding on the bottom dock Column inflated it
by the full keyboard height, pushing it up into the chat content.
Fix: imePadding on the outer Box that contains everything. Now
the entire layout shrinks when keyboard opens — chat area gets
shorter, dock stays at the (new) bottom edge. No overlap.
Two issues prevented locked chats from opening:
1. context as? FragmentActivity silently returned null on some
devices where LocalContext.current is a ContextWrapper. Fix:
walk the ContextWrapper chain to find the FragmentActivity.
2. No fallback when biometrics unavailable — the chat just stayed
locked with no feedback. Fix: if biometrics not available (no
fingerprint enrolled, no hardware), skip biometric prompt and
unlock directly via server token. Biometric is still enforced
when available.
Extracted unlockAndOpen() to avoid duplicating the server unlock
+ message loading logic across biometric/fallback paths.
Root cause: gradient scrim (transparent at top) let chat text
bleed through when imePadding pushed the dock up. Fix: use solid
background color when keyboard is visible, gradient only when
keyboard is closed. Pills and spacer also hidden during keyboard.
Input field: pill shape instead of xl (rounder), higher opacity
(0.85/0.90 instead of 0.75/0.74) for cleaner look — less muddy
glass, more solid surface. Bottom spacer reduced to 4dp when
keyboard open (was 16dp) to sit tight above the keyboard.
Scroll buttons: only one shown at a time based on direction
(up when scrolled down, down when scrolled up), hidden during
streaming. Mutually exclusive instead of both visible.
Sidebar menu: replaced default Material3 DropdownMenu/MenuItem
with custom styled menu — dark surface color (1A2030 dark,
F8F9FB light), KaizenShapes.md rounded corners, 12dp shadow
elevation, custom MenuRow composable with proper spacing.
Three fixes:
1. Token badge only shown when usedTokens > 0 (not in old chats
where we have no streaming data). Pushed to far right with
Spacer weight. Reset to 0 on new chat and conversation switch.
2. Mode pills hidden when keyboard is open (WindowInsets.isImeVisible)
— prevents the bottom dock from pushing too far up and overlapping
chat content. Input field stays compact at the bottom.
3. ModelPill no longer constrained with weight(fill=false) which
was causing layout issues.
Sources are now collapsed by default with a "Quellen (N)" header
that expands/collapses on tap (matching web's SourcesBlock behavior).
Each source renders as a full-width vertical card (not a small
140dp chip) with title, URL in accent color, and optional snippet.
Tapping a source opens it in the browser via ACTION_VIEW intent.
Room crashed on launch with schema hash mismatch because
MessageEntity gained reasoning+sources fields but the DB version
stayed at 1. fallbackToDestructiveMigration(false) only covered
downgrades, not same-version schema changes.
Fix: version 1→2, fallbackToDestructiveMigration(true) so any
future schema change auto-wipes the cache (it's a read-cache,
server is source of truth, rebuilds on next fetch).