ChatInput redesigned: single row (Plus | Text | Tune | Mic | Send).
Pills hidden by default, expandable via tune icon with slide animation
inside the same GlassSurface card. Shape lg (28dp) instead of pill.
ModePillsRow moved from separate floating row into ChatInput as
pillsContent lambda. Bottom dock scrim simplified.
New Chat button (PenLine icon) added to top bar next to TokenBadge.
LazyColumn bottom padding 260→140dp (dock is now smaller).
Text selection: SelectionContainer wraps both user and assistant
messages — long-press to select, copy, share.
Sidebar profile: avatar 30→38dp with radial gradient, rounded lg
shape, icon buttons with subtle backgrounds, bigger text (15sp W400).
Markdown: heading spacing 18→24/20dp, list items better aligned
(8dp gap, bullet muted), blockquote bar 4dp inset + 12dp gap.
Hand-drawn Menu, Plus, Mic, AudioLines, Brain, SlidersHorizontal,
Globe, Image with 1.0px stroke (vs Lucide's 1.5px) to match the
lighter W300 typography. Registered in KaizenIcons — all usages
update automatically, no call-site changes.
Single source of truth for error, success, indigo, link, and dropdown
colors. Replaces ~25 scattered Color(0xFF...) literals across 10 files.
Syntax highlighting and glass-surface tints intentionally left local.
Single button instead of two mutually exclusive ones. Arrow direction
is based on proximity to bottom: near bottom → arrow up (scroll to top),
not near bottom → arrow down (scroll to end). Updates instantly as the
user scrolls. Hidden when already at the target end.
Old logic had an if/else that always showed ArrowUp first (because
!atTop was always true when scrolled down), making ArrowDown unreachable
in most scroll positions.
statusBarsPadding() was on the inner Column (only pushed content down),
but the GlassSurface itself started at a fixed 12dp from the top edge —
on devices with tall notches (S25 Ultra) the sidebar header sat behind
the system clock/icons. Moved statusBarsPadding() to the GlassSurface
modifier so the entire glass panel starts below the status bar.
ReasoningBlock: accent-tinted background + gradient border (matches pill
language), KaizenShapes.md (16dp radius), larger padding, better line
height (20sp), softer icon tint. Replaces flat gray box with solid border.
SourceCards: gradient border instead of solid, KaizenShapes.md, shows
domain instead of full URL in the link line, slightly larger padding.
Both blocks now use the same visual language as the rest of the app —
gradient borders, accent tinting, squircle shapes.
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.
- 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.
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.