Commit graph

189 commits

Author SHA1 Message Date
Bruno Deanoz
f5d7d9a9ab fix Live Call: route audio to speaker with proper communication device API
MODE_IN_COMMUNICATION routes to earpiece by default — no sound on speaker.
The deprecated isSpeakerphoneOn doesn't work on Android 12+.

Fix: enterCallMode()/exitCallMode() lifecycle — sets MODE_IN_COMMUNICATION
+ setCommunicationDevice(SPEAKER) on API 31+ (S25 Ultra = Android 15).
Called BEFORE AudioTrack creation so the routing is active when playback
starts. exitCallMode() restores previous audio mode on call end.
2026-06-24 13:49:04 +02:00
Bruno Deanoz
0baab7db9e fix Live Call echo: proper AEC pipeline for Android voice communication
The model was talking to itself because the mic picked up speaker output.
Three fixes that together enable Android's hardware echo cancellation:

1. AudioManager.MODE_IN_COMMUNICATION — tells the platform this is a
   voice call so the audio routing + AEC pipeline activates
2. USAGE_VOICE_COMMUNICATION on AudioTrack — links playback to the
   capture path so AEC knows what to subtract from the mic signal
3. Explicit AcousticEchoCanceler on the AudioRecord session as fallback

Also enables speakerphone mode (isSpeakerphoneOn) since MODE_IN_COMMUNICATION
defaults to earpiece routing. Cleanup restores previous audio mode.
2026-06-24 13:45:44 +02:00
Bruno Deanoz
1b4cc475f9 design polish: bug fixes, token migration, a11y touch targets
- GlassSurface: derive shadow geometry from shape (pill/circle now curve correctly)
  new shape-aware kaizenShadow(stack, shape) overload in Shadow.kt
  drop cornerRadius parameter from GlassSurface and all 8 call sites

- EmptyHero: replace remember { LocalDateTime.now() } with produceState that
  re-emits on minute boundaries; clock no longer freezes on first composition

- ChatScreen + MeshBackground: skip blob layer + dither while LiveCallOverlay
  is up (renderBlobs flag); avoids two animated MeshBackgrounds compositing
  in parallel

- Color tokens: add KaizenSemanticColors.warning/warningDeep,
  KaizenNeutralWashes (idle/hover/selected), KaizenGlassTint
  (darkTop/darkBottom/lightTop/lightBottom)

- Migrate inline Color(0xFF...) literals to tokens in LiveCallOverlay,
  ChatScreen (version-skew banner), Sidebar (4x neutral washes),
  LoginScreen / PasswordUnlockDialog / SecuritySection (glass tints),
  GlassSurface (use KaizenGlassTint)

- New ui/theme/Spacing.kt: KaizenSpacing scale (xxs-xxl),
  KaizenTouch (minTouch/comfortable/hero), KaizenIconSize (xs-xl)

- Accessibility touch targets:
  Sidebar Settings/Logout 30 -> 44dp
  Sidebar New-Chat 34 -> 44dp
  Sidebar history kebab 28 -> 44dp
  ChatInput Plus/Tune/Mic/Send/Call/Stop 40 -> 48dp

- Remove no-op pointerInput(Unit) {} on KaizenOrb
- Expose SquircleShape.radius (val instead of private) so shadow can read it
2026-06-24 13:39:42 +02:00
Bruno Deanoz
3b32b208b6 fix Live Call: drop all audio chunks during ducking
The old ducking logic only skipped chunks below a threshold (RMS < 0.10),
but the S25 Ultra speakers leak enough audio into the mic to exceed that.
Gemini's server-side VAD picks it up as user speech, triggers interrupts,
and the model starts hallucinating a conversation with itself.

Now: when ducking is active (model speaking), NO mic chunks are sent.
The server-side VAD handles the real speech detection after ducking ends.
2026-06-24 13:06:31 +02:00
Bruno Deanoz
45eb5dfaea fix Live Call: include type field in WS protocol messages
encodeDefaults=false caused kotlinx.serialization to skip the
type="setup" field since it was a default value. The server's
parseClientMessage needs the type field to dispatch — without it
every setup message was rejected as "malformed_message".

Changed to encodeDefaults=true + explicitNulls=false so type is
always serialized but null optionals are still omitted.
2026-06-24 13:00:42 +02:00
Bruno Deanoz
8af2ec8259 fix Live Call: don't shutdown shared OkHttp executor on dispose
LiveClient.dispose() called client.dispatcher.executorService.shutdown()
which killed the shared KaizenApi.baseClient dispatcher. After one failed
call, ALL subsequent OkHttp requests (including normal API calls) got
"executor rejected". Removed the shutdown — cancel() on the WS is enough.
2026-06-24 12:56:39 +02:00
Bruno Deanoz
46a48b6008 fix Live Call crash: Unicode ellipsis in Authorization header
Debug logging used token.take(10) + "…" (U+2026) directly in the
HTTP header value. OkHttp rejects non-ASCII in header values.
Moved the truncation to the log line, header uses the full token.
2026-06-24 12:51:15 +02:00
Bruno Deanoz
408cdaa72d add Live Call debug logging for diagnosing connection issues
Logs WS URL, setup message, every server event (first 200 chars),
onOpen/onClosed/onFailure with codes, and service-level events.
Filter logcat by "LiveClient" or "LiveCallService".
2026-06-24 12:49:11 +02:00
Bruno Deanoz
bf8e591930 fix Live Call: 20s connect timeout + reconnect_failed ends call
CONNECTING state could hang forever if the server never responded
or kept failing silently. Now: 20s timeout ends the call with a
visible error, and reconnect_failed is treated as terminal.
2026-06-24 12:44:01 +02:00
Bruno Deanoz
57cc69c999 remove accidentally committed file 2026-06-24 12:35:17 +02:00
Bruno Deanoz
e5690b2caa fix Live Call: show error details instead of infinite "Verbinde..."
Error events from the WS server were silently swallowed — the onError
handler was empty and provider_error didn't trigger endCall. Now:
errors display in the overlay subtitle and loadError banner, and
provider_error is treated as terminal (ends the call).
2026-06-24 12:34:59 +02:00
Bruno Deanoz
1e5bbf38b9 fix Live Call overlay: opaque MeshBackground instead of transparent scrim
The overlay used a semi-transparent Color.White/Black scrim (alpha 0.85-0.88),
letting the chat content bleed through. Replaced with MeshBackground
(frozen blobs, no animation) for a proper opaque fullscreen call UI.
2026-06-24 12:09:17 +02:00
Bruno Deanoz
0744194c84 Expand auto-search to tech products, movies, series, comparisons
New pattern categories:
- Tech products: iPhone/Galaxy/Pixel/iPad + version numbers, specs,
  release dates, "neuestes Handy", Apple Vision Pro, Meta Quest, etc.
- Movies/series: Staffel/Season/Episode numbers, cast, ratings,
  neue Serie/neuer Film, IMDB, Rotten Tomatoes
- Product comparisons: "vs" with model numbers, "lohnt sich"
- Current leaders: Bundeskanzler, CEO, Präsident

These are high-hallucination topics where the AI's training data
is likely outdated — auto-search gives it fresh facts.

8 new tests (tech, movies, comparisons, leaders).
2026-06-24 10:34:05 +02:00
Bruno Deanoz
feb326fc40 Auto-search detection + live reasoning preview
Auto-search:
- detectSearchQuery() pattern-matches common search prompts in DE+EN
  (weather, news, prices, scores, current events, etc.)
- send() enables webSearch automatically when detected, no manual
  pill needed. Manual pill still works as explicit override.
- 30+ patterns ported from backend lib/chat/detection.ts

Live reasoning preview:
- ReasoningBlock shows first meaningful line as preview label when
  collapsed (max 80 chars, ellipsis). Updates live during streaming.
- "Gedankengang" header + preview line stacked vertically.
- Preview hidden when block is expanded (full text visible).

Tests: 4 new tests for detectSearchQuery (DE patterns, EN patterns,
normal prompts not triggered, short input ignored).
2026-06-24 10:30:52 +02:00
Bruno Deanoz
77d2d8e5df Redesign SourcesBlock — horizontal chips + detail cards
Before: sources hidden behind a collapsed "Quellen (3)" toggle,
vertical stack of flat cards, requires click to see anything.

After:
- Always-visible horizontal scrollable chips with numbered colored
  circles (indigo/violet/cyan/emerald/amber/red/pink/blue cycling)
- Each chip shows the domain name, tap opens the URL
- Search query shown above sources when available
- "Details anzeigen" expands to full cards with colored left bar,
  title, domain, and snippet
- Perplexity/Gemini-inspired compact visual style
2026-06-24 10:23:24 +02:00
Bruno Deanoz
24d6b2148a Add image generation + streaming recomposition isolation
Image generation:
- KaizenApi.generateImage() calls POST /api/v1/image-gen
- Bild mode pill in ChatInput triggers image generation instead of chat
- Generated image displayed as attachment in assistant message
- Conversation created + title generated automatically

Streaming isolation (from previous uncommitted work in ChatScreen):
- liveStream StateFlow emits content during streaming
- Only the active MessageRow subscribes via collectAsState()
- Other LazyColumn items stay completely static during streaming
2026-06-24 10:18:33 +02:00
Bruno Deanoz
f746c603b0 Isolate streaming recomposition to active bubble only
During streaming, content updates now flow through a StateFlow
(liveStream) instead of mutating the SnapshotStateList on every chunk.
Only the single MessageRow with id == streamingMessageId subscribes to
the flow via collectAsState(). All other items in the LazyColumn stay
completely static during streaming.

Before: every chunk mutated messages[idx] → entire LazyColumn evaluated
After: only the live bubble recomposes, list mutation happens once at
stream end

Auto-scroll split into two LaunchedEffects: one for list size changes,
one for liveStream emissions.
2026-06-24 10:15:24 +02:00
Bruno Deanoz
06757d6f69 Update CLAUDE.md with ChatScreenViewModel architecture 2026-06-24 10:10:43 +02:00
Bruno Deanoz
40db869717 Extract ChatScreenViewModel from God Composable
Move all business logic and state (~35 variables, ~15 functions) from
the 1540-line ChatScreen @Composable into ChatScreenViewModel.

ChatScreen is now a thin rendering layer (~430 lines) that reads VM
state and wires UI events to VM methods. Android-framework-only concerns
(ActivityResultLaunchers, DrawerState, LazyListState, keyboard insets)
stay in the Composable.

Changes:
- New ChatScreenViewModel.kt: messages, streaming, conversation mgmt,
  sidebar actions, file upload, voice recording, TTS, live call, unlock
- ChatScreen.kt: pure UI, delegates all logic to vm.*
- Haptics.kt: add createHaptics(Context) factory for non-Composable use
- SessionViewModel: store nullable for test construction
- MainActivity: instantiates ChatScreenViewModel

Tests: 12 new unit tests for ChatScreenViewModel (state transitions,
newChat reset, mode toggles, edit/delete, password dialog lifecycle).
All 90 tests pass.
2026-06-24 10:09:40 +02:00
Bruno Deanoz
89fe5fd2ae Update CLAUDE.md with security + performance audit results 2026-06-24 09:40:26 +02:00
Bruno Deanoz
3b73e15376 Security hardening + performance optimization pass
Security:
- Fix JSON injection in deleteMessage (raw string → serialized DTO)
- Separate OkHttpClient for streaming (30s timeout on normal requests,
  infinite only for chat stream)
- Disable backup (EncryptedSharedPrefs without KeyStore → crash on restore)
- Block cleartext traffic explicitly
- Strip server URLs from log output

Performance:
- Share OkHttp connection pool across KaizenApi + LiveClient
- Eliminate duplicate fetchMe/fetchDefaultModel call on login
- Parallelize prefetchMissing (was sequential per conversation)
- Stream file uploads directly from ContentResolver (no full readBytes)
- Cache assistant message index during streaming (O(1) vs O(n) per chunk)
- Build chat history before adding assistant placeholder (no filter needed)

Bugs:
- Fix send() race condition (isStreaming set before async work)
- Move voice recording file read to IO dispatcher
2026-06-24 09:38:54 +02:00
Bruno Deanoz
392e582084 fix: show toast when Live Call unavailable instead of silent no-op
The call button did nothing when no live model was available (empty
lambda). Now shows a toast explaining the issue.
2026-06-24 07:12:13 +02:00
Bruno Deanoz
da3eded81e Remove temporary bug screenshots + ignore bugs/ dir 2026-06-24 03:27:53 +02:00
Bruno Deanoz
ff66b80966 Wire Live Call end-to-end: turn persistence, model check, permission
Turn persistence: onTurnEnd handler creates conversation if needed,
adds user + assistant messages to UI list, caches in Room. Server
already persists via WS-Server — app only needs local state sync.

Model check: CallButton only triggers Live Call when a vertex:*live*
model is in the catalog (liveAvailable). Falls back to PTT otherwise.

Permission: RECORD_AUDIO runtime request via ActivityResultContracts
before starting the call. Denied → toast, no crash.

Title generation: endLiveCall triggers generateTitle + sidebar refresh,
same pattern as text chat.
2026-06-24 03:25:32 +02:00
Bruno Deanoz
31278f2440 Add Live Call UI overlay + wiring in ChatScreen
LiveCallOverlay — fullscreen composable: KaizenOrb (amplitude-reactive,
streaming/recording variants), duration timer, status text (connecting/
listening/speaking), animated subtitle (transcript/response), end call
button (red gradient circle with PhoneOff icon), interrupt on orb tap.

ChatScreen wiring: CallButton → startLiveCall() → binds LiveCallService
→ Foreground Service with AudioPipeline + LiveClient. Overlay renders
above chat content with fade animation. endLiveCall() unbinds + stops.

ChatInput: onCallClick callback propagated from CallButton to ChatScreen.

i18n: live_connecting/listening/ready/speaking/end/reconnecting in de+en.
KaizenIcons: added PhoneOff (Lucide).
2026-06-24 03:14:08 +02:00
Bruno Deanoz
65ea3a5656 Add Live Call transport layer (P10 foundation)
LiveProtocol — type-safe Kaizen WS protocol serialization/parsing,
mirrors lib/live/protocol.ts 1:1 (setup/resume/audio/text/interrupt/end
+ all 12 server event types).

LiveClient — OkHttp WebSocket client with auto-reconnect (exponential
backoff, max 5 attempts), resume protocol (sessionId + lastSeqId),
backpressure handling, terminal error detection. 15s ping interval
keeps mobile NAT alive.

AudioPipeline — bidirectional audio: AudioRecord (16kHz PCM16 mono,
VOICE_COMMUNICATION for hardware AEC/noise suppression) → 100ms chunks
→ Base64. AudioTrack (24kHz PCM16 mono, LOW_LATENCY, MODE_STREAM) ←
server chunks. Mic-ducking during KI speech (RMS threshold gating).

LiveCallService — Foreground Service (MICROPHONE type), survives
rotation/backgrounding. Notification with duration timer + end button.
Status state machine (IDLE→CONNECTING→LISTENING→SPEAKING). Orchestrates
LiveClient + AudioPipeline, emits onTurnEnd for tree integration.
2026-06-24 03:06:48 +02:00
Bruno Deanoz
25fbb17d18 fix: token badge text wrapping on long model names
Added maxLines=1 and softWrap=false to prevent "0 / 1M" from breaking
into two lines when the top bar is compressed by a long model name.
2026-06-24 03:02:33 +02:00
Bruno Deanoz
bb53b24655 fix: TTS read-aloud downloads audio via OkHttp before playing
MediaPlayer.setDataSource(url) with HTTPS URLs can silently fail due to
certificate issues, redirect handling, or listener race conditions (listeners
were set after prepareAsync). Fix: download the WAV file via OkHttp (same
client as all other network calls) to a temp file, set listeners before
prepareAsync, play from local path. Temp file cleaned up on completion/error.
2026-06-24 01:33:23 +02:00
Bruno Deanoz
287cdb4c6b feat: branch navigation for edit/regenerate instead of destructive delete
Edit creates a sibling branch (same parentId) instead of deleting messages.
Regenerate creates a new assistant branch. BranchNavigator UI shows chevron
arrows with "1/3" counter on messages with siblings. Tree state (activeRootId,
activeChild) synced to server via PATCH. New ChatTree data structure mirrors
the web frontend's tree.ts. StoredMessage.parentId parsed from API response.
2026-06-24 01:29:02 +02:00
Bruno Deanoz
7ba42338c5 docs: add locked chat privacy fixes to CLAUDE.md 2026-06-23 20:24:34 +02:00
Bruno Deanoz
d69e2bcf68 fix: block title-leaking actions on locked conversations
Locked chats could leak their real title through sidebar search,
"Generate Title", and "Duplicate" — all three now blocked.
2026-06-23 20:23:51 +02:00
Bruno Deanoz
7aefd7ba70 fix: migrate ClickableText to LinkAnnotation, add server version check
Replace deprecated ClickableText with LinkAnnotation.Url + Text (Compose
native URI handling). Add GET /api/v1/meta call after login and on each
app resume to detect API version skew — shows dismissible amber banner.
2026-06-23 14:51:20 +02:00
Bruno Deanoz
e6fa8ef8a6 docs: restore completed sections in CLAUDE.md, fix numbering
Keep [x] items as context for future sessions. Added new done items
(text selection, ChatInput capsule, visual refresh). Fixed section
numbering 1-8. Updated media cache to reflect custom disk cache.
2026-06-23 14:40:47 +02:00
Bruno Deanoz
dca9cb7593 docs: update CLAUDE.md with visual refresh + UX overhaul changes
New sections: ChatInput capsule, semantic colors, custom icons,
glass-lens app icon, text selection, flat sidebar profile.
Updated: typography (W300), blob sizes, top bar layout, icon system.
Added full bugs-fixed section for 2026-06-23 design session.
2026-06-23 14:37:53 +02:00
Bruno Deanoz
430d5b609e style: flat sidebar user row — remove heavy card background
No more card background, border, shadow on user profile. Just a flat
row with thin separator line above, avatar 34dp, subtle icon buttons.
2026-06-23 14:31:04 +02:00
Bruno Deanoz
ecb89019dc fix: move new-chat icon left of token badge in top bar 2026-06-23 14:27:24 +02:00
Bruno Deanoz
9f7e3fb8b0 fix: center text and icons vertically in input capsule
Column gets heightIn(min=64dp) + Arrangement.Center so content
sits in the middle. Placeholder maxLines=1 to prevent overflow dots.
2026-06-23 14:24:21 +02:00
Bruno Deanoz
1d47e05dbf style: taller input (64dp), rounder corners (34dp), centered text 2026-06-23 14:20:46 +02:00
Bruno Deanoz
e683419d96 style: taller capsule input matching Gemini proportions
56dp min height, 30dp corner radius, generous inner padding.
Icons 40dp touch targets, 22dp icon size. Text 16sp centered.
High opacity (0.92/0.97) for solid feel. 16dp horizontal margin.
2026-06-23 14:18:23 +02:00
Bruno Deanoz
fb8f21dfd8 style: compact capsule input like Gemini
Tighter row: 36dp icons, 6dp padding, 15sp text, capsule shape.
Higher opacity (0.90/0.96) for solid feel against background.
All action buttons (Send/Call/Stop) matched to 36dp.
2026-06-23 14:02:37 +02:00
Bruno Deanoz
e56bef05f6 style: rounder input card, stronger shadow, higher opacity
pill shape (32dp radius), level4 shadow for stronger separation from
background, tintAlpha 0.88/0.94 (more opaque, cleaner), tighter
horizontal margin (12dp).
2026-06-23 13:59:12 +02:00
Bruno Deanoz
4958ca8429 feat: single-row input, pills inside card, new-chat button
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).
2026-06-23 13:57:28 +02:00
Bruno Deanoz
ad98b9f499 feat: redesign app icon + splash to match glass-lens orb
Foreground: opaque plastic orb → translucent glass-lens with subtle
amber tint, lighter speculars, thinner rims. 10 layers → 8 layers.

Backgrounds: warm cream → neutral off-white (light), cleaner obsidian
(dark). Matches app's actual background colors.

Monochrome: cleaner gradient, thinner rim.

Splash screen uses the same foreground via windowSplashScreenAnimatedIcon.
2026-06-23 13:48:25 +02:00
Bruno Deanoz
1c9491e3dc feat: text selection, modern sidebar profile, markdown spacing
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.
2026-06-23 13:45:23 +02:00
Bruno Deanoz
95672d3124 fix: mode pills more visible, action buttons larger, revert Brain icon
Pills: higher background alpha, stronger borders, removed muted text
opacity, bigger padding (14x8dp), icon 16dp, text 13sp W400/W500.

Action buttons: Plus/Mic/Call/Send/Stop 38dp→42dp, icons 19→22dp.

Brain icon reverted to Lucide — custom version too complex at 16dp.
2026-06-23 13:11:47 +02:00
Bruno Deanoz
3c6a8911c4 feat: custom 1.0px icons for highest-visibility touchpoints
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.
2026-06-23 13:05:50 +02:00
Bruno Deanoz
0f6e55d77a feat: upgrade NetworkImageCache — disk cache, shared client, crossfade
Shared OkHttpClient via KaizenApi.imageClient (reuses connection pool),
file-based disk cache in cacheDir/img (survives app restart), 32MB
size-based memory limit instead of 50-entry count, Crossfade animation
on image load.
2026-06-23 13:00:49 +02:00
Bruno Deanoz
38678a2add refactor: extract hardcoded colors into KaizenSemanticColors
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.
2026-06-23 12:56:09 +02:00
Bruno Deanoz
75ca8c0e7b feat: visual refresh — lighter typography, message animations, richer themes
Typography: body text W400→W300 for airier feel, headings scaled up
(H1 26sp, H2 22sp, H3 18sp), W600/W500 weight hierarchy for contrast.

Animations: animateItem() on LazyColumn messages — 350ms fade-in,
spring placement, 200ms fade-out.

Themes: blobs 20% larger + better full-screen distribution, higher
alpha on OLED (0.48 peak), light mode factor 0.65→0.78, reduced
vignette, distinct primaryDeep per theme for richer gradients.
2026-06-23 12:22:38 +02:00
Bruno Deanoz
fbead82ada fix: increase chat content padding to prevent overlap with top/bottom bars 2026-06-23 07:17:12 +02:00