A new assistant message was seeded with the previous message's
reasoningText (and isReasoning flag) via onMessageStart, and
resetStreaming() never cleared the shared streaming state. If the
next reply produced no reasoning, the stale trace stayed glued to
the new bubble and got persisted to Room.
- onMessageStart: new message + StreamingState start with empty
reasoning instead of inheriting the previous turn's
- resetStreaming: clears isReasoning + reasoningText on the shared
state (finalized messages keep their own copy for the card)
- regression tests: MessageStart seeding, no-reasoning persistence,
reset clearing, finalized-message preservation
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix(chat): context meter reflects compression via session.context_breakdown RPC
The chat context meter read GET /api/sessions/{id} input_tokens — a
cumulative lifetime counter that compression never resets, so the meter
only ever grew and sat red after the first compaction.
Mirror the desktop: read live occupancy from the session.context_breakdown
WS RPC (context_used = compressor last_prompt_tokens, falling back to an
estimate of the live system prompt + tools + compacted history). The
compressor zeroes last_prompt_tokens when it runs, so the meter now drops
right after auto-compaction or /compress, exactly like desktop/web.
REST getSessionDetail is kept solely to feed the detail sheet's cumulative
token accounting. RPC failures keep the last known values; the meter never
blanks. Fixes#756.
* feat(chat): compressed xN badge on the context widget
The context chip now shows how many times the current session has been
context-compressed, sourced from the session.usage WS RPC (compressions =
context_compressor.compression_count) — the same usage snapshot the desktop
status bar reads. The badge explains a meter drop after auto-compaction or
/compress: the widget no longer just shows the number, it shows the story.
- UsageSnapshotResponse model + parseUsageSnapshot (Number-safe like the
breakdown parser — WS parser decodes JSON numbers as Double)
- compressionCount added to ChatUiState, reset on session switch/create/branch
- session.usage fires alongside session.context_breakdown in the 5s poll;
failures keep the last known badge value (best-effort, never blank)
- 3 parser tests; full local suite 615 tests green
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat: register as Android default digital assistant
* fix: assistant role request fallback + always-attempt click
* remove: assistant role request row from Settings
The system-level assistant registration stays (VoiceInteractionService
+ ROLE_ASSISTANT, set via system Settings -> Default apps), but the
in-app Settings row is dropped: RequestRoleActivity fails to resolve
the caller on a plain startActivity ('Package name cannot be null or
empty'), so the button was a dead end. Removes the row, the
AssistantRole helper, and the now-unused strings.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(chat): add inline video player component and modal viewer
Closes#722
* feat(chat): modernize video player with compact lock pill, save, and share
* refactor(chat): simplify inline video thumbnail to launch fullscreen player on tap
* fix(chat): pass session cookie and auth token headers to VideoView for protected media playback
* fix(chat): format unquoted cookie header for VideoView auth
* fix(chat): resolve video URLs via resolveImageUrl for gateway-hosted media
Clears stale context token states on session switch/creation and fetches
fresh context usage info immediately instead of waiting for the 5s poll.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Use MaterialTheme.colorScheme tokens for Assistant, System, and Tool bubbles so that all presets and dynamic colors render properly without theme bleed.
* feat(chat): full-screen image viewer (issue #723)
Tap any chat image (markdown , inline attachment thumbnail,
or bubble image) to open a full-screen viewer:
- pinch-zoom + pan via native detectTransformGestures
- downswipe-to-dismiss at min zoom
- Save -> device Downloads (MediaStore), Share -> Android share sheet
(both fully device-local; never sent back to the Hermes server)
Plumbing: onImageClick(ImageViewerModel) flows MarkdownText ->
ChatBubble/InlineAttachment -> ChatMessageList -> ChatScreen (state +
ImageViewerDialog). ImageBytesResolver reuses the existing authed
OkHttpProvider path for gateway http(s) sources; data:/content://
handled locally. No new dependencies.
Closes#723
* fix(chat): resolve strings at composable scope to satisfy LocalContextGetResourceValueCall lint
Lint forbids reading resource *values* from LocalContext.current inside
coroutine lambdas. Hoist all viewer strings via stringResource() at
composable scope and reuse them in onSave/onShare.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix(#724): inline host-path MEDIA: images on mobile (client-side)
The previous approach re-fetched the REST session-messages endpoint, but that
endpoint returns the raw MEDIA:<path> text (api_server._message_response does
not inline, and the transcript stores raw content). The desktop app instead
reads the file locally via readFileDataUrl because it is co-located with the
host. Mirror that on mobile: on assistant message.complete/done, inline any
MEDIA:<path> image into a base64 data:image/... markdown tag the renderer can
show. Mobile-only, backend untouched. Unreachable tags are stripped (graceful
degrade). Pure inlining logic extracted to MediaInline for unit testing.
* fix: correct RegexOption name (IGNORE_CASE) in MediaInline
* test: fix byte literal types in MediaInlineTest
* fix(#724): rewrite host-path MEDIA: to gateway /api/files/download URL
Pivot from the earlier base64-inline approach to the desktop-proven,
remote-phone-working fix: rewrite MEDIA:<path> directives (delivered raw over
the WS stream) into authenticated
 markdown tags,
mirroring desktop media.ts mediaExternalUrl. The mobile renderer (MarkdownText)
shows http(s) ![]() via Coil, so agent-sent images now render on mobile too —
including on a remote phone (real HTTP, unlike a host-local file read).
Mobile-only, backend untouched. Pure rewrite logic in MediaUrlRewriter
(replaces MediaInline) with unit tests.
* feat(#724): full gateway-file support — attach MEDIA: files (not just images)
Pivot the MEDIA: fix from image-only URL rewriting to real attachments:
- Attachment: add gatewayUrl + AttachmentSource (LOCAL/GATEWAY) so
gateway-hosted files are first-class and render via existing InlineAttachment.
- HostMediaExtractor (replaces MediaUrlRewriter): parse ANY absolute MEDIA:
directive (any extension) and strip them from message text.
- ChatViewModel.attachHostMedia: build the authed /api/files/download URL via
GatewayFileClient, classify via MediaTypes, attach as Attachment (images
inline, every other file tappable). Idempotent.
- InlineAttachment: tappable. Gateway files fetch via GatewayFileClient and
share via a chooser Intent (FileProvider); images load the URL in Coil.
- Hence agent-delivered images, audio, video, CSV, PDF and arbitrary files all
work on mobile — including a remote phone. Backend untouched.
Builds on #726 (GatewayFileClient). Rebased onto main after #726 merged.
* fix(#724): resolve imports, bubble callback scope, and quoted media path regex
* feat(chat): open attachment in-place on tap (#724)
Tap opens LOCAL (content://) and GATEWAY (fetch->FileProvider) files via
ACTION_VIEW; failures surface as a non-blocking snackbar via openError.
* fix: remove redundant out-of-order io.mockk.* wildcard import (ktlint CI)
ktlint import-ordering requires lexicographic order; the trailing wildcard
'import io.mockk.*' sorts before 'io.mockk.coEvery' and was redundant — every
mockk symbol is already explicitly imported, and the single mockkConstructor
call uses a fully-qualified io.mockk. prefix (needs no import).
Fixes ktlint failure on PR #725.
* fix(ci): mock android.jar Intent stub so openAttachment GATEWAY test passes
android.jar unit-test stubs Intent.setDataAndType/addFlags to throw
'... not mocked'. openWithView builds an Intent and calls them, so
openBytes' runCatching swallowed the exception and startActivity never
fired, failing the test. Mock the Intent constructor + setters and
capture the constructed instance to assert the ACTION_VIEW contract.
* fix(#724): resolve host media download URLs and deduplicate REST synced chat bubbles
* fix(#724): add FLAG_ACTIVITY_NEW_TASK for file intents and update GatewayFileClientTest
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat: add Files tab backed by the managed-files endpoint
Add a Hermes Mobile tab that browses the gateway's managed-files root via the
existing /api/files* backend routes: list/navigate folders, breadcrumb path,
create folder, upload (streaming multipart), delete, and open files via the
authenticated GatewayFileClient download URL.
- data/model/ManagedFile: entry + list/read/upload/mkdir/delete models
- HermesApiService: list/read/upload(+stream)/mkdir/delete endpoints
- FilesViewModel: state, navigation, per-row busy, transient-state cleanup
- FilesScreen: folder list, breadcrumb, FAB new folder, upload picker,
delete confirm, open via system viewer
- NavigationKeys + ScreenRegistry + strings: register in Inspect drawer section
* fix(files): open files in-app via FileProvider instead of browser URL
The previous file-open path handed a ?token=-authenticated URL to the system
browser, but /api/files/download only accepts the ephemeral in-process session
token as ?token=, not the mobile's Bearer token — so the request failed and the
browser rendered the JSON error body instead of the file.
Fix: download the file in-app through the authenticated client (Bearer/
session-cookie, same path as the rest of the app), decode the base64 data_url,
write it to cacheDir, and open it via FileProvider.getUriForFile + ACTION_VIEW
(mirrors the existing ChatScreen/SystemViewModel open-from-cache pattern).
Works on a remote phone since the file is materialized locally first.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat: gateway files client — fetch any gateway-hosted file over HTTP
Add GatewayFileClient: a typed client for the gateway's managed-files
download endpoint (GET /api/files/download?path=<enc>&token=<enc>), which
streams the raw bytes of any file living on the gateway host. This is the
mobile equivalent of desktop's mediaExternalUrl() and works on a remote
phone (real HTTP, not a host-local file read).
- buildDownloadUrl(): pure authed-URL builder (null on blank creds /
non-absolute path), mirrors desktop's token-in-query auth.
- fetch(): OkHttp GET via the shared client, maps 401/403/404/413 to typed
GatewayFileResult variants; returns GatewayFile(name, mime, bytes).
- MediaTypes: IMAGE/AUDIO/VIDEO/FILE classification by extension (mirrors
desktop MEDIA_BY_EXT) so future consumers can present images, audio,
video, or generic files — beyond just images.
Pure builders/classifiers are unit-tested. Mobile-only, backend untouched.
This is the foundation PR; issue #724's MEDIA: rendering will consume it.
* fix: decode filename + handle download-URL in mediaNameFromPath
- GatewayFileClient.parseFilename now URL-decodes the captured value so
filename*=UTF-8''a%20b.png yields 'a b.png'.
- MediaTypes.mediaNameFromPath extracts the filename from the percent-encoded
path= query param of a full /api/files/download?path=<enc> URL (the real
case), not just a bare path.
* fix(remote): fix unit tests for gateway file client and media parsing
Replace plus-encoded spaces with %20 in download URL builder, URL-decode filenames in parseFilename, and extract target filename from URL path parameters.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Assistant messages could not display inline images or attachments on the
mobile client. The markdown renderer had no image block, and
AssistantBubble only rendered MarkdownText + timestamp, ignoring
message.attachments (only UserBubble did).
- Add MdBlock.Image + standalone  parsing in MarkdownText.
Accepts http(s) gateway URLs and data:image/...;base64 inline media.
- Render message.attachments inside AssistantBubble (mirrors UserBubble)
via InlineAttachment / AsyncImage.
- Add parser unit tests for image blocks (url, data: URL, inline-in-paragraph).
Local verification: ./gradlew ktlintCheck compileDebugKotlin assembleDebug
all green; app-debug.apk produced.
Fixes feature gap where agent-delivered media (e.g. MEDIA:/ images
sent to chat) was invisible in the Android app.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* Adopt /api/health REST route (issue #713)
Cheap liveness probe on the connection screen: GET /api/health surfaces
backend version + auth mode (Token vs Gated/OAuth) without the heavier
/api/status payload. Added HermesApiService.getHealth + HealthStatus model;
ConnectViewModel.loadHealth probes on open (best-effort, silent on failure);
ConnectScreen shows a read-only health card when reachable.
Verified: ktlint clean, assembleDebug BUILD SUCCESSFUL locally.
* Add used/full context meter to chat screen
Shows session prompt tokens vs model context window above the composer,
polled every 5s. Color-coded bar warns at 70%/90%.
* fix(chat): fix unit tests for context usage meter
Format >=100k tokens cleanly in ContextUsageChip and stub AuthManager profile ID in ChatViewModelTest.
* Fix context meter: source used tokens from input_tokens
Gateway /api/sessions/{id} has no last_prompt_tokens column; use
input_tokens instead. Show meter as soon as full context is known.
* Add tappable context breakdown sheet
Chip opens a bottom sheet with input/output/cache/read/write/reasoning
token rows + message count. Source breakdown from /api/sessions/{id}.
* Refresh context meter on streaming complete
Trigger fetchContextUsage immediately when a message finishes streaming
instead of waiting for the 5s session-sync poll.
* Fix context meter: source used tokens from live WS session.info
The REST /api/sessions/{id} endpoint reads the DB input_tokens column,
which is stale/0 for active sessions — that's why the meter was stuck at 0.
Switch the meter to the live values the gateway pushes over the WS
session.info payload after each turn:
- used = last_prompt_tokens (real used context for the turn)
- full = context_length (model-accurate window; keep /api/model/info as a
fallback before the first turn completes)
fetchContextUsage now only tops up the denominator from /api/model/info and
no longer overwrites the live WS value with the stale REST input_tokens.
The 5s poll remains as a fallback.
* Revert "Fix context meter: source used tokens from live WS session.info"
This reverts commit 932393fd80993d57e66fe93552a949460d646674.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* Add session.redirect affordance (issue #710)
Route send through session.redirect while a turn is streaming and the prompt
is plain text (no attachments, not a /command); otherwise prompt.submit.
Adds WsMethods.SESSION_REDIRECT + HermesWsClient.sendRedirect. No separate
button — the composer already sends mid-turn, so send just does the redirect.
* fix(chat): capture typing state before sendMessage for redirect
Capture wasStreaming state before updating uiState.isAgentTyping so normal sends call prompt.submit.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Flush throttled reasoning before state-transition events (MessageStart/
Complete/Done/ToolStart) so a reasoning delta arriving just before a
transition is not dropped. Also compare reasoningText in sameMessages so
a reasoning-only restore actually re-merges.
- ChatStreamingController.flushPendingReasoning() forces a synchronous flush
- handleWsEvent calls it for the transition events
- sameMessages now includes reasoningText in its equality check
- tests: ChatStreamingControllerTest (flush + no-resurrect), reasoning survives
MessageDone after an unflushed delta
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Fix active saved profile visibility in Catppuccin light mode and Monochrome light/dark modes by adjusting primaryContainer contrast and adding a primary border stroke.
* fix(mcp): make Add Server + Catalog headers read distinctly from buttons
Both section headers reused the shared SectionHeader, which paints the
title in primary/bold — so each header row looked like a tappable
button next to its trailing TextButton. Add a scoped McpSectionHeader
with a distinct leading icon (➕ Add / 🗄 Explore) and a neutral
onSurface bold title, so they read as headers and look different
from each other.
* fix(mcp): give section header row + action button distinct backgrounds
- Whole header row now sits on a surfaceContainerLow rounded (12dp) surface
so it reads as a contained section header, not bare text.
- The trailing New/Browse action uses secondaryContainer (onSecondaryContainer
text) — a clearly different color from the row background, so button
vs header are visually distinct.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(keys): overhaul Keys screen UI and UX
Fix card padding alignment, add delete confirmation dialog, add FAB with AddKeyDialog, support copying revealed values, add status badges, and pin restart banner.
* fix(keys): fix scaffold actions parameter and missing string resource
Resolve compilation errors in KeysScreen by removing floatingActionButton from HermesScaffold and adding missing keys_action_open_url resource.
* feat(keys): replace dropdown headers with horizontal category filter chips
Add CategoryFilterBar featuring horizontal FilterChips for quick category filtering (All, LLM Providers, Tool API Keys, Messaging Platforms, Agent Settings) with total key counts.
* style(keys): enhance key card padding, spacing, borders, and surface contrast
Use surfaceContainerLow with subtle outlineVariant border and 16dp rounded corners for high contrast cards, and set 12dp list spacing between cards.
* fix(keys): wrap key title and status badge in FlowRow to prevent layout overflow on long key names
Prevent status badge from squishing or overflowing when displaying long API key names by using a FlowRow that wraps gracefully.
* fix(keys): add file-level OptIn for ExperimentalLayoutApi
Fix compilation error by adding @file:OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) to KeysScreen.kt.
* fix(keys): fully qualify class names in file-level OptIn annotation
Fix unresolved reference build error by fully qualifying ExperimentalMaterial3Api and ExperimentalLayoutApi in @file:OptIn before package statement.
* fix(keys): remove invalid verticalAlignment parameter from FlowRow
* fix(keys): optimize long API key name readability and wrapping
Insert zero-width spaces after underscores to ensure clean word-boundary wrapping, add adaptive typography (titleSmall with 20sp line height for long names), and stack StatusBadge cleanly below the title.
* feat(keys): make API key names and values single-line and horizontally scrollable
Enforce singleLine=true / maxLines=1 with horizontalScroll(rememberScrollState()) for both key names and revealed key values so cards remain compact with uniform heights.
* feat(keys): keep single-line scrollable key values and clean multi-line key names
Keep secret key values single-line and horizontally scrollable, while restoring clean multi-line word-boundary breaking (at underscores) for key names.
* style(keys): format KeysScreen.kt with ktlint
Tighten FilledTonalButton contentPadding on the MCP server card action
row so the Restart label fits beside the icon in the weighted half-width
slot (default 24dp horizontal padding was eating the text).
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(skills): redesign tab switcher and fix status filter alignment and card padding
* fix(skills): replace status filter chips with SingleChoiceSegmentedButtonRow for precise alignment
* fix(skills): left-align status, category, and source filter chip rows
* fix(skills): explicitly left-align status FilterChips with 16dp start margin
* fix(skills): match status filter chip row styling and All label to category row below it
* fix(skills): remove extra Row wrapper padding from status filter chips in SkillsScreen
* feat(skills): enhance card background contrast and reduce side padding for Installed and Hub cards
* feat(skills): reduce card/search padding to 8dp and pin top controls with weight(1f) on LazyColumn
* style(skills): fix ktlint import-ordering and remove unused imports (PR #704)
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* perf: optimize synchronous I/O in AuthLoginViewModel
Replace blocking OkHttp .execute() with a suspending .await() extension
(CallExt.kt) backed by suspendCancellableCoroutine, freeing Dispatchers.IO
threads during network waits. Probe/connect paths made suspend.
- Call.await() guards both onResponse/onFailure with continuation.isActive
to avoid AlreadyResume on cancellation.
- Removes non-asserting AuthLoginViewModelBenchmark timing dump.
* perf: optimize synchronous I/O in AuthLoginViewModel
Replaced the blocking `.execute()` OkHttp calls with a suspending `.await()` extension function inside `AuthLoginViewModel`. This frees up the `Dispatchers.IO` threads by deferring the blocking operations to OkHttp's internal task runner, providing a safer async approach and preventing Thread pool starvation on slow network responses.
Modified `probeDashboardInternal` and `connectBasicAuth` to be proper `suspend` functions and resolved corresponding unit tests.
* fix(test): add missing import io.mockk.any in AuthLoginViewModelTest
* fix(test): fix mockk imports and Log matchers in AuthLoginViewModelTest
* fix(test): use persistent mockProbeClient in AuthLoginViewModelTest
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
In gated mode (basic-auth cookie flow) the dashboard 401s any request that
carries an Authorization: Bearer header alongside the valid session cookie
(verified live 2026-07-23). ApiClient.authInterceptor stamped the WS ticket
as a Bearer header on every REST call, so every non-chat tab (skills, cron,
config, tools, mcp, ...) failed with "token expired" while the WS chat
(?ticket=) kept working.
- Add AuthManager.isGatedMode() (wsAuthParam == "ticket").
- authInterceptor skips the Bearer header in gated mode; loopback/token
behavior unchanged.
- TokenRefreshAuthenticator bails in gated mode instead of attempting the
loopback-only SPA token refresh.
- ApiClientTest: add gated/loopback regression tests. 7/7 pass, ktlint clean.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* perf: Hoist regexes in MarkdownText.kt to avoid recompilation
Resolves a performance issue where `bulletRe`, `taskRe`, and `orderedRe`
were instantiated and compiled inside the `parseBlocks` function on every
call, causing unnecessary overhead during repeated markdown parsing (e.g.,
when chat messages stream in).
These regex objects have been hoisted to file-level `private val`
constants (`BULLET_RE`, `TASK_RE`, `ORDERED_RE`), ensuring they are compiled
only once when the class is loaded.
A benchmark `MarkdownTextBenchmark.kt` was added to verify the
performance change, showing an approximate 50ms improvement over 10k
iterations.
* perf: Hoist regexes in MarkdownText.kt to avoid recompilation
Resolves a performance issue where `bulletRe`, `taskRe`, and `orderedRe`
were instantiated and compiled inside the `parseBlocks` function on every
call, causing unnecessary overhead during repeated markdown parsing (e.g.,
when chat messages stream in).
These regex objects have been hoisted to file-level `private val`
constants (`BULLET_RE`, `TASK_RE`, `ORDERED_RE`), ensuring they are compiled
only once when the class is loaded.
A benchmark `MarkdownTextBenchmark.kt` was added to verify the
performance change, showing an approximate 50ms improvement over 10k
iterations.
* perf: Hoist regexes in MarkdownText.kt to avoid recompilation
Resolves a performance issue where `bulletRe`, `taskRe`, and `orderedRe`
were instantiated and compiled inside the `parseBlocks` function on every
call, causing unnecessary overhead during repeated markdown parsing (e.g.,
when chat messages stream in).
These regex objects have been hoisted to file-level `private val`
constants (`BULLET_RE`, `TASK_RE`, `ORDERED_RE`), ensuring they are compiled
only once when the class is loaded.
A benchmark `MarkdownTextBenchmark.kt` was added to verify the
performance change, showing an approximate 50ms improvement over 10k
iterations.
* Test ServerCollectionOps data manipulation utilities
Added comprehensive unit tests for `ServerCollectionOps.kt` to cover all scenarios for `addOrUpdate`, `selfHealed`, and resolution properties (`resolvedBaseUrl`, `resolvedHost`, `resolvedPort`). Also updated the function name `addOrReplaceServer` to `addOrUpdate` to match the intended signature in the issue description, and updated affected call sites.
* Fix ktlint formatting issues and ensure build succeeds
Fixed ktlint errors in `ServerCollectionOpsTest.kt` by correctly formatting arguments and expressions to comply with project style guidelines. Ran `assembleDebug` and tests locally to verify build health.
* fix(#688): route expired auth sessions to sign-in
Adopt global 401 latch, WS disconnect, and Back/profile guards.
* chore: clean up formatting noise
* fix(ktlint): reorder imports to place java and kotlinx at end
* fix(ktlint): place java.io.IOException at end of imports
* test: fix NavBackStack type parameter in AuthenticationRoutingTest
* test: add AuthSessionState tests to AuthLoginViewModelTest
* fix(ktlint): fix import order in AuthLoginViewModelTest
* fix(#686): implement robust history pagination
Honor server-returned offset, degrade gracefully on older servers, and parse JSON tool results.
* fix(ci): repair unit-test compilation and pagination test mocks
- Remove trailing blank line in SessionMessagesResponse.kt (ktlint)
- Replace unresolved io.mockk.eq/coAnswers usage in ChatViewModelTest with
an ordered-call counter inside coAnswers, which compiles and runs on
MockK 1.14.11 (eq is a matcher-scope member, not a top-level import)
- Align HermesApiServiceMockWebServerTest with the PR's JsonElement content
model (contentText) and the new include_compacted=true default query param
All unit tests pass locally with the ByteBuddy agent preload.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Introduce ChatScrollController owning all chat scroll behavior:
- continuous bottom-follow intent tracked from LazyListState (not only on data change)
- pixel-precise bottom detection (last item edge vs viewport, small tolerance)
- streaming follow that pauses on upward scroll and never forces the reader down
- tail updates keyed on messages/streaming/thinking/subagent/clarify
- unread (new-message) count shown on the scroll-to-bottom FAB, cleared on tap
- paging anchor preserved by message id across older-history prepend
- all scroll commands (send/FAB/session switch/search/paging) serialized via one scope
Closes#682
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(navigation): regroup Kanban to AUTOMATE and Gateway to INSPECT in navigation drawer
* feat(navigation): change Achievements screen icon to EmojiEvents (trophy)
* perf(model): optimize model tab load speed with two-phase fetch and loadQuick
* perf: optimize data loading across Config, System, Profiles, and Billing tabs
* chore: remove leftover bottom nav references and unused updatePrimaryScreens
* style: fix ktlint violations in Config/Profiles/Billing ViewModels
- property/argument-list wrapping for async(...) calls
- multiline-expression wrapping for async blocks
- remove unused import in ConfigViewModel
- break long error-message string literals to stay under 120 cols
---------
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
* ui: input bar reasoning chip tweaks
- Remove emoji from reasoning level labels (dropdown + chip text)
- Reduce reasoning chip horizontal padding (8dp -> 4dp)
- Move reasoning chip to right side (after spacer, next to mic)
- Increase model chip minimum width (120dp -> 140dp)
- Remove unused width import
* fix(ui): add missing widthIn import in ComposerToolbar
Resolves 'Unresolved reference widthIn' compile error at
ComposerToolbar.kt:95 that broke CI (unit-tests, android-lint,
instrumented-tests all fail on compileDebugKotlin).
* ui(chat): enhance input bar layout, reasoning menu header, and card shadow
Give ChatInputBar a floating rounded container with shadow contrast. Make model chip take available space and add header to reasoning menu.
* fix(ui): remove trailing blank line in ComposerToolbar for ktlint
Fix standard:no-consecutive-blank-lines ktlint violation.
* ui(model): enhance ModelPickerDialog with rounded cards and provider badges
Apply card styling, rounded corners (24dp dialog, 12dp cards), provider badges, and shadow contrast to ModelPickerDialog.
* feat(model): add in-dialog model pinning to ModelPickerDialog
Add pin/unpin icon buttons to model cards in ModelPickerDialog across both ModelScreen and ChatScreen.
* ui(model): remove provider badge chip and eliminate bottom whitespace in ModelPickerDialog
Replace bottom Cancel button with top-right Close icon to eliminate bottom whitespace. Remove inline provider badge chip from model cards.
* perf(model): optimize ModelPickerDialog load time with lazy items and O(1) set lookup
Lazy-register individual model items in LazyColumn and memoize pinnedSet to prevent synchronous rendering lag when opening dialog.
---------
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
When the initial REST page returns empty messages, hasOlderMessages
was still set to true (offset > 0), causing an infinite loop of empty
load-more requests. Guard with && chatMessages.isNotEmpty().
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
* feat(chat): rich message bubbles with reasoning, code, clarify, subagent cards (#659#660#661#662#663#664)
* fix(chat): render ClarifyBubble at bottom of message list, not as inline SYSTEM message
Fixes two on-device bugs:
1. ClarifyBubble now renders at the very bottom of the LazyColumn
(after all messages, streaming message, typing indicator, and subagent
indicators) instead of inline among regular messages.
2. Bubble disappears when an option is selected or dismissed — the
ClarifyBubble is driven by state.clarifyRequest instead of a
persisted ChatMessage.clarifyInfo, so clearing the request state
removes it from the list entirely.
* fix(chat): add text input field to ClarifyBubble for custom typed answers
* fix(chat): render ReasoningCard at list level with stable key, not inside AssistantBubble
* fix(chat): address agy review — hoist regex, fix a11y, cache anim specs, disable animateContentSize while streaming
* fix(chat): replace hardcoded Color() with theme constants to pass CI checkColorLiterals
---------
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
* feat(chat): add reasoningLevel to ChatUiState + ViewModel methods
* feat(chat): add ComposerToolbar with model + reasoning chips
* refactor(chat): rewrite ChatInputBar to two-row layout
* feat(chat): wire ComposerToolbar into ChatScreen
* fix(chat): make model chip fixed 120dp with horizontal scroll for long names
* feat(chat): reasoning chip opens dropdown menu with all 9 levels instead of cycling
* fix(chat): remove Auto level from reasoning menu — backend only supports none..ultra, default is medium
* feat(chat): sync model + reasoning level from backend session.resume response
* fix(chat): handle session.info events for model/reasoning sync + fix null value 4002
* fix(chat): safety timeout for createNewSession + try-catch event loop
* fix(chat): address agy review findings
- Restore InsertDriveFile icon for non-image attachments
- Add widthIn(max=120.dp) constraint on attachment filename
- Restore stringResource reference for remove-attachment description
- Remove dead cycleReasoningLevel() method (dropdown supersedes)
- Fix createNewSession timeout race with generation counter
* fix(chat): update instrumented test for two-row layout
chatScreen_renders_and_acceptsInput had an assertion that the mic
button disappears when text is entered — this was the OLD single-row
layout behavior. The new two-row composer always shows the mic button
in the toolbar row, independent of input state.
---------
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
* fix(chat): make chat input composer more compact and flush
* fix(chat): fix chat page compose structure (issue #652)
- Resolve EmptyState rendering alongside empty LazyColumn by wrapping in if/else
- Flatten nested weight(1f) layout by moving search bar to ChatScreen
* style(chat): run ktlint auto-format and fix missing padding import
* style(chat): remove unused imports in ChatComposer and ChatMessageList