The save/share/close icons and the busy spinner were tinted with
colorScheme.onPrimary, which stays white under dynamic light schemes,
against the viewer's scrim-colored background. Switch to onSurface,
the token designed for content over non-primary surfaces.
Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
Deletes the ChatMessageList/ChatStreaming bubble path and the ChatStyle
setting (issue #866 phase 6). ChatScreen now renders FullBleedChatList
directly; ChatBubble becomes the user-message anchor with the shared
SystemBubble/InlineAttachment/formatTimestamp helpers intact. Stubs of
AuthManager.getChatStyle() removed from four test suites; TodoTaskCardTest
renders ToolBubble directly.
Verified: ktlintCheck green, testDebugUnitTest 962/962 pass, assembleDebug
APK builds.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(chat): ChatStyle preference type + store field + AuthManager (issue #866)
* feat(settings): chat style toggle in Appearance (issue #866)
* feat(chat): plumb ChatStyle into ChatUiState + refreshSettings (issue #866)
* feat(chat): full-bleed turn grouping (issue #866)
* feat(chat): full-bleed agent message renderer (issue #866)
* feat(chat): full-bleed compact tool rows + system events (issue #866)
* feat(chat): full-bleed chat list renderer + streaming (issue #866)
* feat(chat): wire full-bleed renderer behind chat style setting (issue #866)
* test(theme): full-bleed contrast gate across all presets (issue #866)
Gate caught NORD light primary/background < 3:1 — header role label
switched to onSurfaceVariant (meta token, correct for header + passes
in every preset; NORD's pastel Frost accent can't reach 3:1 on light).
* test(chat): full-bleed renderer coverage (issue #866)
* style(chat): ktlint formatting pass on full-bleed renderer (issue #866)
* fix(chat): eagerly capture per-item state in full-bleed list (issue #866)
LazyColumn item{} lambdas run at composition time, so loop locals read
inside them saw final values: turn header never rendered (firstProseSeen
always true at compose) and milestones misindexed. Capture showTurnHeader
+ milestone as immutable vals during the DSL loop. Caught by fresh-context
review (decompiled Compose foundation to confirm item composition timing).
* test(chat): stub AuthManager.getChatStyle in mockkObject setups (issue #866)
refreshSettings()/loadSettings() now read AuthManager.getChatStyle();
the four test files that mockkObject(AuthManager) with explicit stubs
had no stub for the new method, so mockk fell through to the real
object and threw 'AuthManager not initialized'. Full suite: 953/953
passing, same shape as pristine origin/main.
* feat(settings): move chat style toggle from Appearance to Chat section
* feat(chat): hoist reasoning above tool rows in full-bleed turns
Tool rows stream in before the answer message, so the turn rendered
tools above the reasoning card. The turn's reasoning now renders at
the top of the agent turn, with tool rows and prose underneath.
Streaming is folded into the turn model (groupIntoTurnsWithStreaming)
so the hoist works live while the answer is still streaming, and the
detached streaming item is gone (which also fixed the duplicate
turn-header quirk for streaming continuations).
* feat(chat): cap reasoning card height + long-press to copy
Expanded trace caps at 40% of the screen with internal scroll (Show
full lifts the cap); long-press anywhere copies the reasoning.
* feat(chat): blinking typing caret while streaming
* feat(chat): make full-bleed the default chat style
* feat(chat): remove thinking dots, show agent header above reasoning
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* chore: eliminate all Kotlin compiler warnings in main source
* test(chat): fix cross-class coroutine leaks poisoning the unit suite
- ChatUpdateCommandTest: inject FakeSlashUsageStore — the real DataStore
store inits on real IO threads with a relaxed-mock app (cacheDir NPE)
and leaks an uncaught exception into the next runTest class.
- ChatUpdateCommandTest + SlashCommandDispatchRpcTest: mock
ProfileSwitchCoordinator.switched (ChatViewModelTest pattern) — VMs
otherwise park collectors on the singleton flow that crash later
classes (ProfileSwitchCoordinatorTest) on stale-Main resumption.
Full suite: 944 tests, 0 failures (was 2 flaky per run).
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
- CronJob model gains optional monitor_script/monitor_url (additive-safe)
- Job editor: Monitor mode section (off/script/url) with XOR source clearing;
disabled while no-agent is set (backend invariant); new jobs apply monitor
via create -> follow-up PUT with rollback delete on failure (the REST create
model drops the fields)
- Job list/details: blocked_config renders as a distinct error badge
('Blocked (config)'), no_change as informational; monitor source shown in
run details
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Backend now supports order=latest on GET /api/sessions/{id}/messages
(offset measured back from the newest message, page returned
chronologically). The mobile initial load previously computed its anchor
from a possibly-stale session-list message_count, which landed the first
page at the wrong position (dropped history or a duplicated tail).
- HermesApiService.getSessionMessages: add @Query("order")
- Initial load requests the newest page directly (order=latest, offset=0)
and drops the fetchServerMessageCount offset math; legacy backends
(no pagination echo) fall back to the count-based anchor
- Backward paging advances the from-end offset by a full page; a short
page marks the oldest boundary and stops pagination
- ChatMessage stable keys under latest paging use the server's
AUTOINCREMENT row id (from-end positions shift as the transcript
grows and would collide across hydrations)
- syncCurrentSession refetches the newest page and merges by row id
with a logical-content fallback for re-keyed rows
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix(sessions): show Messages stat + slim prune button
The Active stat card always rendered 0: the mobile model declared
'val active: Int = 0', a key GET /api/sessions/stats never sends
(backend returns total/active_store/archived/messages/by_source), and
with ignoreUnknownKeys = true deserialization silently fell back to 0.
Rework the stats row instead of just key-mapping:
- Messages card replaces Active, reading the 'messages' key
(db.message_count(), web_routers/sessions.py:523-550)
- Prune becomes a slim 52dp icon-only card (contentDescription keeps it
accessible) so the Messages card keeps width for large counts
* feat(sessions): compact stat counts (100k / 1.23m) for Total & Messages
formatCompactCount renders counts as max 3 digits + unit letter:
987 -> '987', 100987 -> '100k', 1234567 -> '1.23m', 100000000 -> '100m'.
Applied to both the Total and Messages stat cards so large numbers fit
the row. Covered by SessionsStatsFormatTest (5 test cases).
* fix(sessions): label prune + empty-cleanup buttons, drop top select toggle
- Prune card now sizes to its label (icon + 'Prune' text) instead of a
52dp icon-only square, staying compact via wrap-content width.
- Empty-session cleanup becomes a labeled FilledTonalButton (icon +
'Delete empty sessions', count badge kept) instead of a bare icon.
- Removed the top-bar select toggle (SelectAll icon); selection mode
remains reachable via long-press > Select on any session row.
- Dropped now-orphaned content_desc_enter/exit_selection strings (en/ko).
* fix(sessions): prune card wrap-content width + shorter cleanup label
The prune card dropped its fixed 52dp width but kept fillMaxSize on the
inner Box — as an unweighted Row child that expands to the full row
width, starving the weighted Total/Messages cards (reported broken on
device). The Box now fills height only, so the Card wraps to its
content (icon + 'Prune').
Empty-session cleanup button label shortened to 'Delete empty'.
* fix(sessions): stack prune label under icon, cleanup button reads 'Empty'
- Prune card: trash icon above the 'Prune' label (column layout),
still wrap-content width so Total/Messages keep their space.
- Empty-session cleanup button label shortened to 'Empty'.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix(chat): stop duplicating interim commentary before tool calls (issue #842)
Interim commentary the agent narrates before each tool call rendered
TWICE. Two mechanisms, both now covered:
1. The old tool.start orphan seal + message.complete's full text: when the
complete payload repeats the commentary, the final bubble duplicated
it. onMessageComplete now strips the sealed-orphan prefix (tracked in
StreamingState) from the final text.
2. On-device capture + DB forensics (2026-08-08) exposed the real
repeat offender: the app seals the RAW streamed text — which carries
leading blank lines the model emits before its narration — while the
backend persists a CLEANED copy (leading whitespace stripped). A
transcript reload ~10s later merged the REST page, exact-content
matching failed, and the cleaned REST copy rendered on top of the
live bubble. sameLogicalMessage now compares trimmed content, so the
sealed live bubble is covered by the REST row.
Also:
- session.interrupt seals the in-flight streaming message before
clearing streaming state, so interrupting a commentary turn keeps the
partial text
- regression tests cover both turn shapes (commentary in the final row →
stripped; absent → kept) and the whitespace-drift reload case
* fix(chat): match tool rows by gateway call id, drop reasoning-echo dupes (#842)
REST transcript rows carry tool_call_id; live WS bubbles keep tool_id from
tool.start. Match tool rows on that 1:1 identity so MCP/web rows (stored as
raw <untrusted_tool_result> text with no JSON key) dedupe during the
post-turn transcript merge instead of ghosting after the final message.
reasoning.available echo (narration text == message body, sent ~40ms before
each tool.start) no longer overwrites the real reasoning trace, so narration
bubbles don't render their own text twice (body + Reasoning card).
Room v5 migration adds tool_call_id; regression tests use captured
on-device payload shapes.
* fix(chat): flush throttled tokens before orphan seal + prefix-safe dedupe (#842)
The tool.start seal raced the 33ms throttled token flush: deltas landing just
before the transition stayed in the buffer, so the sealed narration orphan came
out truncated (on-device capture: orphan ended '...chickpea', backend persisted
'...chickpea goodness:') and failed the logical-match against the clean REST
row — the commentary ghosted on the next transcript sync.
- ChatStreamingController.flushPendingTokens(): force-flushes the throttled
TOKEN buffer on MessageStart/MessageComplete/MessageDone/ToolStart so the
reducer seals the COMPLETE narration.
- sameLogicalMessage: accept prefix-covering (both sides >=40 chars) so a
residual seal drift can never re-add a substantial narration.
- Regression tests: controller flush + truncated-orphan prefix match.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* refactor(theme): unify all presets behind one palette template
* docs(theme): update THEMES.md and AGENTS.md for the palette template
* fix(theme): restore per-preset error slot contrast, merge StatusColors into HermesStatusColors
- onErrorContainer becomes an explicit per-preset field (was derived from
onError — same-color pairs in Gruvbox/Catppuccin light made error text
invisible; 6 screens render this slot)
- Default dark + AMOLED onError -> White (were <3:1 on their error color)
- drop StatusColors + statusColorsOf mapper; PaletteColors.status is typed
HermesStatusColors directly (identical field lists, pure copy ceremony)
- ThemePalette getters now by lazy; schemeFor/statusFor accessors;
requireNotNull on the default-theme fallback (DefaultTheme is FULL)
- resolvers internal for testability
* test(theme): contrast, mode-validation, and fallback contract tests
- every shipped mode's onError/error + onErrorContainer/errorContainer
pair meets >= 3:1 WCAG contrast (kills the error-slot regression class)
- ThemeMode init validation rejects illegal palette combinations
- AMOLED darkOnly contract + light-mode fallback to Default light
* refactor(theme): Catppuccin — named swatches from the official palette spec
Hex values verified identical (zero color drift): repeated colors (Crust,
Surface0/1, Base, Text...) now live once as named swatches matching
catppuccin.com; bespoke light primary container tints stay local.
Mocha/Latte pair unchanged.
* refactor(theme): Gruvbox — authentic bright/faded accent scheme with named swatches
Redesign per gruvbox spec: bright accents on dark bg0-bg4, faded accents
on light bg0-bg4 (correct contrast play for light mode). Named swatches,
single Gray shared across modes. Dark error pair fixed for >= 3:1
(onErrorContainer -> DarkFg1 cream, was bright red on Bg3 = 1.87:1).
* feat(theme): replace default preset with Slate — modern slate blues + teal accents
The brand default (voltage purple/amber) becomes Slate: bright teal/blue
accents on dark bg0-bg4, faded variants on light bg0-bg4. Named swatches,
single neutral gray shared across modes. Slate is also the fallback for
darkOnly/lightOnly presets (AMOLED light mode).
* feat(theme): add Nord preset — Polar Night/Snow Storm surfaces, Frost/Aurora accents
Nord's single accent set is reused across both modes (pastel accents stay
light, so on* colors use Polar Night dark text in both modes — documented
in-file). Surface ladder shares outer tiers (Nord ships 4 dark + 3 light
tones for 5 tiers). Dark error pair fixed for >= 3:1 (onErrorContainer ->
Nord6, was Nord11 on Nord2 = 2.0:1). Wired into ThemePreset, the settings
picker, and docs; default label now reads Default (Slate).
* refactor(theme): Monochrome + AMOLED — pure grayscale ramp, true-black AMOLED variant
Monochrome: every swatch R=G=B, roles by lightness only (status colors
included — documented trade-off: pair with icons/labels). AMOLED: dark-only
true-black variant (pure #000000, OLED pixels off; #121212 baseline of
Monochrome doesn't do that), containers collapse where nothing darker
exists. Both ship maximum-contrast grayscale pairs (12.8:1+ on error
slots).
* chore(theme): drop Neon Noir, finalize docs + picker label
- remove NeonNoirScheme.kt + ThemePreset.NEON_NOIR everywhere (dispatcher,
picker, strings EN/KO, tests, AGENTS.md, THEMES.md) — 6 presets remain:
Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Nord
- picker label for the default preset is now plain 'Default'
- THEMES.md: preset conventions section (named swatches, bright/faded
split, grayscale-status caveat, contrast test gate) + verify step runs
testDebugUnitTest
* fix(theme): make drawer icon colors visibly follow the active preset
Selected icon now tints with the theme's primary accent (teal for Slate,
orange for Gruvbox, mauve for Catppuccin, frost-blue for Nord) instead of
onPrimaryContainer — the new presets' containers are all dark gray with
near-white text, so the selected row looked identical across themes.
Unselected icon/text colors pinned explicitly to onSurfaceVariant.
* chore(ui): remove lightning emoji from the app
- drawer title + About name: '⚡ Hermes' -> 'Hermes' (EN + KO)
- skill markers (chat self-improvement badge, memory graph): ⚡ -> 🛠
- in-progress indicators (subagent inspection): ⚡ -> 🔄
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix(chat): meter flashes stale window on mid-chat model swap (issue #817)
session.info updates the model label as soon as the backend applies a
swap, but the context meter's denominator kept the old model's window
until the next fetch — the chip briefly rendered the new label over the
old window (e.g. 1M flashing before settling on 262k).
- session.info with a DIFFERENT model now blanks the denominator (chip
hides while resolving) and immediately re-fires fetchContextUsage()
- fetchContextUsage writes the denominator ONCE, atomically: RPC
context_max wins, REST model/info is the fallback — a stale pre-swap
response can never override a fresh one
- regression tests: swap re-fires the fetch and lands the new window;
an identical session.info leaves the meter untouched
* fix(chat): don't fall back to profile REST window on model swap (issue #817)
After a session-scoped swap the live agent's context_breakdown can
return empty (context_max 0) while the runtime warms up. The refetch
then fell back to GET /api/model/info — PROFILE-scoped — which reports
the profile's main-model window (e.g. 1M), resurrecting the stale
window under the new label. The swap-triggered refetch now trusts only
the RPC's live context_max; the chip stays hidden until the real
window lands.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
MainActivity is exported (launcher requirement) and consumed the
session-id extra from any incoming intent without checking the action —
a foreign app could launch it explicitly and force-navigate to a chat
screen. Guard consumeNotificationIntent() on the app's own notification
action and hoist the action string into a shared const so
ChatNotificationService and MainActivity can never drift.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
The gateway replays the full transcript over a new socket on
session.resume unless omit_messages is set; mobile hydrates history via
REST in parallel, so the replay is redundant. When a reconnect lands
mid-stream, the replayed message gets a fresh UUID and dedup can't
collapse the 90% orphan with the 100% replay (different ids + content),
leaving a duplicate bubble. Desktop already sends omit_messages: true;
mirror that in both resume paths (ChatViewModel + NotificationReplyReceiver)
and pin it in the exact-map test verifies.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(sessions): empty-session cleanup button + latest-descendant resume + fresh-session 404 fix (issues #787, #840)
- Sessions top bar: Delete-empty icon with count badge, grey/disabled when 0
(GET /api/sessions/empty/count + DELETE /api/sessions/empty, confirm dialog)
- Resume flow resolves GET /api/sessions/{id}/latest-descendant first —
branched sessions continue in the newest child leaf, not the stale parent
(best-effort; falls back to the requested id)
- syncCurrentSession guarded by sessionHasServerPresence — a freshly created
(never prompted) session has no server row yet; the REST transcript 404s
and the resume retry spun forever (logcat 404 evidence)
* fix(sessions): clamp message-count fallback to backend limit cap (422)
fetchServerMessageCount requested limit=500 but the backend caps
session list page size at 100 (sessions.py Query le=100) — every
fallback scan 422'd (seen in device logcat after a branch). Sessions
are ordered recent, so the target is always in the top page.
* test(slash): make dispatch-capture assertions order-independent (CI flake)
The capture-race flake (SlashCommandDispatchRpcTest, hit 3x across CI and
local runs 2026-08-07): tests asserted on methodSlot.captured — the LAST
request captured — but the create flow's session.usage/context_breakdown
requests can land after the slash-dispatch one depending on suite order
(the class's own comment documents this cross-wiring). Converted the
three capture-based tests to capture ALL requests and find the target
record by method, with dispatch-before-fallback ordering for the 4018
test. Same assertions, no dependence on request interleaving.
* test(slash): convert remaining capture-race assertions (flake round 2)
The capture race moved to the focus/init tests (:424) after the first
three were fixed — every request-capture test in the class used the
same last-captured assumption. Converted the remaining five (init,
init+arg, focus, focus on, type=send) to the find-record-by-method
pattern; the type=send answers block reads methodCalls.last() since the
capture precedes the answer. Zero request() slot captures remain.
* test(slash): blocklist assertion counts command RPCs only (flake round 3)
The blocklisted-command test counted ALL request() calls and asserted
zero — the create flow's session.usage/context_breakdown noise can land
in the window after the stub registers (the same capture race that hit
the dispatch tests). Asserted instead that no COMMAND_DISPATCH or
SLASH_EXEC fires, which is the actual blocklist contract. E2E's static
Dispatchers mock stays (E2E VMs need IO routed to the test dispatcher —
removing it broke 20 E2E tests).
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(memory): dedicated Memory screen — providers, builtin files & reset (moved from System tab)
* feat(memory): move self-improvement & learning activity section from System tab to Memory screen
* fix(memory): remove top padding, single-line reset buttons, providers spacing
* fix(memory): tighten provider card padding
* fix(memory): distinct AutoStories icon for Memory tab
* chore: ignore local debug apk
* feat(ui): fresh icon set for drawer screens
* feat(ui): custom Neurology icon for Memory tab
* fix(ui): theme-driven fill for NeurologyIcon (checkColorLiterals)
The custom Neurology vector used SolidColor(Color.Black), tripping the
hardcoded-color guard (issue #622). The fill only contributes alpha —
Icon()'s tint replaces the RGB at draw time, mirroring Material's own
icon set — so the literal belongs in theme/ like every other hardcoded
color: new IconDefaultFill token, scoped there by the guard's design.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(toolsets): per-toolset config/env/provider/post-setup management (#782)
* fix(toolsets): per-entry toolset name for shared detail ViewModel (#782)
NavDisplay uses the activity-level ViewModelStoreOwner, so class-keyed
viewModel{} returns the same ToolsetDetailViewModel for every detail
entry; the constructor-captured name froze the first-opened toolset into
all entries (on-device: every toolset showed web's providers, no further
/config calls after the first). The screen now drives setToolset(name) +
loadConfig() per entry, and setToolset resets per-toolset state
(config, revealed secrets, running post-setup) so nothing bleeds across
toolsets.
* style(toolsets): single-line scrollable tool chip strip (#782)
A wrapping FlowRow stacked chips vertically for toolsets with many
tools (web etc.), ballooning card height — especially on large font
scales. Chips now sit on one horizontally scrollable row.
* chore(toolsets): drop stray terminal-noise line from chip strip change
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* test(connect): pin restart-with-scoped-profile token resolution (phase 5)
* fix(profiles): scope switch via un-clamped active-profile state
The switch persisted the server profile through serverStore.selectedProfileId,
whose selfHealed() clamps ids absent from the local connection list — server
profiles like "meow" never exist there, so the scope silently reset to
default and the re-dialed socket created the fresh session in the wrong
profile (live logcat 2026-08-06: session.create carried profile=default
right after switching to meow).
- AuthManager: prefs-backed activeProfileId state, immune to the clamp,
restored on startup; resetAuthStateForTest clears it
- ProfileScopeInterceptor + WsProfileParams + ChatViewModel fetchContextUsage:
read the active profile instead of the connection selection
- ProfileSwitchCoordinator: write activeProfileId; dial the socket on
Dispatchers.IO (the ticket mint did blocking I/O on the main thread —
NetworkOnMainThreadException — and fell back to a 1s reconnect)
- ProfilesViewModel: inject the IO dispatcher; ProfilesViewModelTest drops
its static Dispatchers mock (JVM-wide bleed class)
- Tests: WsProfileParams/Interceptor tests use the real prefs-backed state
instead of mockkObject(AuthManager) (re-mock breaks in suite order after
real AuthManager init); AuthManagerTest + Issue647ProfileUrlTest cancel
the init() DataStore scope in tearDown (UncaughtExceptionsBeforeTest
phantoms); regression test pins the clamp on the old path vs the new state
* fix(profiles): resolve getter/property JVM collision; de-poison slash test
- AuthManager.activeProfileId (StateFlow) and a getActiveProfileId()
function generated the same JVM getter name — calls silently resolved to
the property getter at runtime (ReadonlyStateFlow -> String
ClassCastException, CI unit red 2026-08-06). Function removed; callers
read activeProfileId.value.
- ChatViewModel: inject ioDispatcher (38 sites) so SlashCommandDispatchRpcTest
can drop its mockkStatic(Dispatchers) bleed without racing real IO.
- SlashCommandDispatchRpcTest: drop the static Dispatchers mock (JVM-wide
bleed; CI cross-wired COMMAND_DISPATCH captures in suite order), construct
the VM with the test dispatcher.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(profiles): make profile switching actually scope the app (issue #781 follow-up)
* fix(profiles): inherit connection token when switching to same-dashboard profile (issue #781 follow-up)
* fix(chat): reset open session on profile switch + prefill login profile (desktop parity)
* fix(ws): inject profile into profile-scoped RPC params so chat follows the selected profile
* test(chat): de-poison SlashCommandDispatchRpcTest (drop static Dispatchers mock)
mockkStatic(Dispatchers::class) + a stubbed Dispatchers.IO bleeds JVM-wide and
breaks later classes in suite order (CI 2026-08-06: COMMAND_DISPATCH captures
cross-wired → ComparisonFailure on lower stack PRs). ChatViewModel gains an
injected ioDispatcher (38 sites); the slash test constructs the VM with the
test dispatcher so the IO stub is unnecessary.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
- New-job dialog gains a Start-from dropdown: pick a catalog blueprint, fill its typed slots (time/enum/weekdays/text), save instantiates it via POST /api/cron/blueprints/instantiate
- Delivery field is now an editable picker fed by GET /api/cron/delivery-targets (origin + local + connected platforms)
- Blueprints and targets fetched on editor open; 422 slot errors surface the backend detail
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(profiles): delete, rename, auto-describe and setup command (issue #781)
* feat(profiles): use shared ModelPickerDialog for set model (issue #781)
* fix(profiles): move overflow menu to card header so it can't be clipped (issue #781)
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
DropdownField (ConfigScreen) and PluginProviderDropdown (PluginsScreen)
were byte-for-byte identical. Extract into ui/common/ExposedDropdownField
and route both call sites to it. No behavior change.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Orphaned duplicate of SkillsScreen's private InstalledSkillsView. Zero
references in the repo (no imports, call sites, or tests). Shared helpers
(CATEGORY_ALL, SkillFilter, SkillsUiState) live in SkillsScreen.kt /
SkillsViewModel.kt, not this file. No behavior change.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* perf: remove runBlocking from PersistentCookieJar to fix overhead
* perf: remove runBlocking from PersistentCookieJar to fix overhead
* perf: fix PersistentCookieJar race condition using CountDownLatch
* perf: remove runBlocking from PersistentCookieJar to fix overhead
* Extracted the inline regex in htmlPathFromInlineDiff to a pre-compiled private constant to optimize CPU utilization during high-frequency loop iterations.
* Fix ktlint standard:multiline-expression-wrapping rule in ToolViewBuilder
Added `setPackage(packageName)` to the explicitly defined reply intent
before wrapping it in the mutable PendingIntent for remote input.
RemoteInput intents strictly require `FLAG_MUTABLE` so the Android OS
can append the user's text reply. To use `FLAG_MUTABLE` securely
and prevent unintentional component delivery, explicit component
boundaries (like `setPackage`) must be defined.