Compare commits
2 commits
ad11c0ee46
...
34a527cdb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a527cdb6 | ||
|
|
4351428c66 |
2 changed files with 62 additions and 25 deletions
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -228,7 +228,7 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
|
|||
| `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) |
|
||||
| `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 uses `PickMultipleVisualMedia(ImageAndVideo)`, file uses `OpenMultipleDocuments` — gallery + files support multi-select |
|
||||
| `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 |
|
||||
|
|
@ -244,6 +244,13 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
|
|||
| `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 |
|
||||
| `15f5781` | **Biometric bypass fix** — closed 3 auth bypass paths in `openConversation()` + sidebar `ToggleLock`. Unified `requestBiometricOrPassword()` gate, `PasswordUnlockDialog` fallback, server `POST /api/v1/auth/unlock` now verifies password (Argon2id) when provided |
|
||||
| `ba7ebaf` | **Security settings** — biometric toggle (persisted in SecureStore), signed-in devices list with remote revoke (`GET/DELETE /api/v1/auth/tokens`), password change form (`POST /api/v1/auth/change-password`) |
|
||||
| `e62c525` | **App icon redesign** — 10-layer glass-lens orb (soft halo, 6-stop amber body, cool glass overlay, bounce light, volume shadow, caustic structure, elliptical specular + micro pinpoint glint, gradient rims). Improved backgrounds (light/dark) + monochrome |
|
||||
| `1929073` | **Unified top bar** — merged hamburger circle + ModelPill into single GlassSurface pill (menu icon → dot divider → model name 13sp Medium, 180dp max ellipsis). No chevron |
|
||||
| `1362dae` | **Snappy sidebar** — optimistic insert (new chat appears instantly with first-line preview), `generateTitle` returns title directly + Room `updateTitle`, `replaceAll` uses upsert+deleteExcept (no empty-list flash) |
|
||||
| `68721d8` | **KaizenIcons system** — centralized icon registry (`ui/icon/KaizenIcons.kt`), Lucide 1.5px stroke as base (same as web), custom override layer for brand icons. All 10 source files migrated, zero Material Icon imports remaining |
|
||||
| `ad11c0e` | **Multi-select file picker** — gallery uses `PickMultipleVisualMedia(ImageAndVideo)`, file picker uses `OpenMultipleDocuments`. Multiple items selectable at once |
|
||||
|
||||
**Earlier UI/feel work (Phase 0 prototype → feel-first):**
|
||||
- Liquid glass styling, floating panels, glassmorphic sidebar
|
||||
|
|
@ -274,6 +281,14 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
|
|||
- **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.
|
||||
|
||||
### Bugs fixed (2026-06-22 — security + UI session)
|
||||
|
||||
- **Locked chats bypassed biometric auth** — three paths in `openConversation()` called `unlockAndOpen()` without authentication when biometrics unavailable. Sidebar `ToggleLock` sent `PATCH { locked: false }` without auth. Fix: unified `requestBiometricOrPassword()` gates all unlock paths; `PasswordUnlockDialog` fallback; server verifies password via Argon2id.
|
||||
- **New chats not appearing in sidebar** — conversation only showed after stream + save + title + server refresh (seconds of delay). Fix: optimistic Room insert immediately after `createConversation()` with first-line preview as title.
|
||||
- **Title generation not working immediately** — `generateTitle` was fire-and-forget, `refreshConversations()` raced with server-side generation. Fix: `generateTitle` now returns the title from the response; `updateTitle()` writes directly to Room.
|
||||
- **Sidebar flicker on refresh** — `replaceAll()` did `DELETE ALL` + `INSERT`, causing Room Flow to emit empty list briefly. Fix: `upsert` + `deleteExcept` (no flash).
|
||||
- **Gallery/file picker single-select only** — `GetContent()` allowed only one item. Fix: gallery → `PickMultipleVisualMedia(ImageAndVideo)`, file → `OpenMultipleDocuments(["*/*"])`.
|
||||
|
||||
## What's NOT done yet (app, priority order)
|
||||
|
||||
### 1. Remaining visual/UX gaps (daily-drive blockers)
|
||||
|
|
@ -296,12 +311,15 @@ These are what separate "prototype" from "daily-driver":
|
|||
|
||||
- [ ] **Auto-logout on 401** — if the token is rejected, route to login screen instead of showing error banner
|
||||
- [ ] **Server version check** — call `/api/v1/meta` after login to detect server skew and warn cleanly
|
||||
- [x] **Biometric lock** (`BiometricPrompt` + KeyStore/CryptoObject) — implemented for conversation unlock; `setUserAuthenticationRequired(true)` + `setInvalidatedByBiometricEnrollment(true)` ensures hardware-enforced biometric, not bypassable even on rooted devices
|
||||
- [x] **Biometric lock** (`BiometricPrompt` + KeyStore/CryptoObject) — hardware-enforced, password fallback dialog when unavailable, biometric toggle in Settings, all unlock paths gated
|
||||
- [x] **Security settings** — biometric toggle, signed-in devices with remote revoke, password change
|
||||
- [x] **Password change** — `POST /api/v1/auth/change-password` (Argon2id, rate-limited)
|
||||
- [x] **Device management** — `GET/DELETE /api/v1/auth/tokens` (list + revoke, can't revoke self)
|
||||
- [ ] **`FLAG_SECURE`** on sensitive screens — see `LATER.md` §1B
|
||||
|
||||
### 4. Features for parity with the web
|
||||
|
||||
- [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 (multi-select for gallery + files), 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] **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
|
||||
|
|
@ -338,6 +356,7 @@ okhttp = "4.12.0"
|
|||
kotlinxSerialization = "1.7.3"
|
||||
securityCrypto = "1.1.0-alpha06"
|
||||
biometric = "1.4.0-alpha02"
|
||||
composeIcons = "1.0.0" # com.composables:icons-lucide-android (Lucide for Compose)
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
|
|
|||
|
|
@ -532,48 +532,66 @@ fun ChatInput(
|
|||
) {
|
||||
val cs = MaterialTheme.colorScheme
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val accent = LocalKaizenAccent.current
|
||||
|
||||
val hasContent = value.isNotBlank() || pendingFiles.any { it.uploaded != null }
|
||||
val canSend = enabled && hasContent && pendingFiles.none { it.uploading }
|
||||
|
||||
val iconBg = if (isDark) Color.White.copy(alpha = 0.07f) else Color.Black.copy(alpha = 0.05f)
|
||||
|
||||
GlassSurface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
.padding(horizontal = 16.dp),
|
||||
shape = KaizenShapes.pill,
|
||||
tintAlpha = if (isDark) 0.85f else 0.90f,
|
||||
shadowStack = KaizenShadows.level2,
|
||||
cornerRadius = 28.dp,
|
||||
tintAlpha = if (isDark) 0.82f else 0.88f,
|
||||
shadowStack = KaizenShadows.level3,
|
||||
cornerRadius = 32.dp,
|
||||
) {
|
||||
Column {
|
||||
Column(Modifier.padding(horizontal = 14.dp, vertical = 14.dp)) {
|
||||
if (pendingFiles.isNotEmpty()) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(start = 14.dp, end = 14.dp, top = 10.dp),
|
||||
Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(bottom = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
pendingFiles.forEach { pf -> PendingFileChip(pf, onRemove = { onRemoveFile(pf.id) }) }
|
||||
}
|
||||
}
|
||||
Row(Modifier.padding(start = 6.dp, end = 8.dp, top = 10.dp, bottom = 10.dp), verticalAlignment = Alignment.Bottom) {
|
||||
Box(Modifier.size(42.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick), contentAlignment = Alignment.Center) {
|
||||
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
|
||||
Box(Modifier.fillMaxWidth().heightIn(min = 48.dp), contentAlignment = Alignment.CenterStart) {
|
||||
if (value.isEmpty()) Text(
|
||||
stringResource(R.string.chat_input_placeholder),
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
BasicTextField(
|
||||
value = value, onValueChange = onValueChange,
|
||||
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 24.sp),
|
||||
cursorBrush = SolidColor(accent.primary), maxLines = 6, modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
Modifier.size(40.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onAddClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Box(Modifier.weight(1f).heightIn(min = 42.dp).padding(horizontal = 6.dp), contentAlignment = Alignment.CenterStart) {
|
||||
if (value.isEmpty()) Text(stringResource(R.string.chat_input_placeholder), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 15.sp)
|
||||
val accent = LocalKaizenAccent.current
|
||||
BasicTextField(
|
||||
value = value, onValueChange = onValueChange,
|
||||
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 24.sp),
|
||||
cursorBrush = SolidColor(accent.primary), maxLines = 6, modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(
|
||||
Modifier.size(40.dp).clip(KaizenShapes.circle).background(iconBg),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Mic, stringResource(R.string.chat_call), tint = cs.onSurfaceVariant, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
if (hasContent) {
|
||||
SendButton(enabled = canSend, onClick = onSend)
|
||||
} else {
|
||||
Box(Modifier.size(44.dp).clip(KaizenShapes.circle), contentAlignment = Alignment.Center) {
|
||||
Icon(KaizenIcons.Mic, stringResource(R.string.chat_call), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue