- Display a subtle centered system pill '[⏩ {bot} passed]' when a bot passes
- Style with Material icon (Icons.Filled.FastForward) and surface tokens
- Keep pass indicators strictly ephemeral to avoid polluting LLM prompt context
- Exclude from cross-device snapshot sync so it disappears on restart/rehydrate
- Add English and Arabic localized string resources
- Update GroupChatViewModelTest to verify pass indicator presence and LLM filtering
Co-authored-by: Luna <luna.m57@agentmail.to>
* chore(ui): replace emoji with Material icons across chat UI
Removes emoji glyphs and swaps them for real Material icons / plain text:
- ToolSchemaRegistry: iconEmoji String -> ImageVector icon for all ~35 tools;
summaryPrefix emoji stripped (terminal keeps '$ '); consumer renders Icon().
- ToolBubble: collapsed summary now shows Icon(config.icon) instead of an emoji
Text prefix; web-search url drops the link emoji; warning badge uses
Icons.Filled.Warning instead of a '⚠' Text glyph.
- ChatBubble SelfImprovementReviewCard: 🛠/🧠 String -> Build/Psychology Icon.
- ChatViewModel / ChatWsEventReducer: drop ⚠️/✅ prefixes from user-facing
status/error messages (severity already conveyed by color/styling).
- SubagentInspectionSheet: ✅/🔄/❌/⭕ status markers -> CheckCircle/Autorenew/
Cancel/RadioButtonUnchecked (tinted); ElectricBolt section headers ->
FormatListBulleted + Groups.
- strings.xml (en/zh/ko): drop '✓' from the Copied label.
ktlint clean; compileDebugKotlin SUCCESSFUL. Remaining chat files
(ToolViewBuilder, ChatComposer, MessageCards, ChatReactions, ComposerToolbar,
FullBleedAgentMessage) tracked in follow-up commits on this branch.
* test: align unit tests with emoji-free summary + status messages
composeToolSummaryLines dropped the emoji parameter and no longer prepends
an emoji glyph to the collapsed summary line, so update ToolSummaryLinesTest
call sites + expected strings accordingly.
SlashCommandDispatchRpcTest asserted the old '⚠️ /cmd: ...' error messages;
those prefixes were removed in the UI cleanup, so drop them from the
expected strings.
ChatWsEventReducerTest emoji usages are model-output fixtures, not assertions
on changed code paths — left untouched.
* feat(chat): compact sticky task progress chip (#942)
Re-add a glanceable progress strip above the chat timeline while work is
active. Distinct from the inline TodoTaskCard (permanent timeline record):
this chip is the temporary "what is happening right now" surface.
- Shows only while todos are incomplete or a subagent is running.
- Bounds to the same hydrated state.todos / subagentIndicators the card
uses -- NOT the old broken state.todos gate removed in #811.
- Label: "Tasks 2/5 · <in-progress task> · N agent" (agent count when relevant).
- Tap opens SubagentInspectionSheet + resumes bottom-follow.
- Auto-hides when all work completes/cancels; rebuilds on session resume.
- i18n strings added (en/zh/ko); ktlint clean; compileDebugKotlin + unit
tests green.
* refactor(chat): drop inline TodoTaskCard, keep single sticky progress chip
Per follow-up to #942: the inline todo card in the chat view is removed. The
sticky TaskProgressChip above the timeline is now the only glanceable surface;
the tool message itself remains the permanent record.
- Remove inline TodoTaskCard early-return in ToolBubble.kt (todo tool output
now renders like any other tool bubble).
- Delete TodoTaskCard.kt + TodoTaskCardTest.kt (SecurityRiskChip lives in
ToolBubble.kt, so risk display is unaffected).
- Redesign chip: replace the ElectricBolt icon with a real
CircularProgressIndicator (no emoji), keep ChevronRight affordance.
- Label handles the no-todos case (subagents only): 'N agent running'.
- Drop dead todo_task_count string; add task_progress_agent_running (en/zh/ko).
ktlint clean; compileDebugKotlin + chat unit tests green.
* chore: clear pre-existing compile warnings
- HermesWsClient.kt: drop unnecessary safe call on non-null ResponseBody
(resp.body.string()) and the now-dead 'body != null' guard it implied.
- SessionsScreen.kt: replace deprecated Icons.Filled.CallSplit with the
AutoMirrored variant (match existing Send usage in this file).
ktlint clean; compileDebugKotlin SUCCESSFUL with no warnings.
* chore(chat): replace emoji status markers with Material icons in inspection sheet
The sticky progress chip opens SubagentInspectionSheet on tap; that sheet used
emoji Text glyphs for status. Replaced with real Material icons:
- Todo rows: completed -> CheckCircle (success), in-progress -> Autorenew
(primary), cancelled -> Cancel (warning), pending -> RadioButtonUnchecked
(muted).
- Subagent rows: complete -> CheckCircle (success), failed -> Cancel (error),
running -> Autorenew (primary).
- Section headers: ElectricBolt -> FormatListBulleted (agent plan) + Groups
(subagents).
Icons tinted via LocalHermesStatusColors. No emoji remain in the chip's tap
surface. ktlint clean; compileDebugKotlin SUCCESSFUL.
* fix(lint): drop orphaned todo_task_count translation in ko
Removing the string from the default (en) locale left a stale Korean
translation, which tripped Lint's ExtraTranslation error and aborted
lintDebug. Removing the orphan so default + translations stay in sync.
android-lint now BUILD SUCCESSFUL (0 errors).
* fix(chat): address #943 review - progress count, resume hydration, chip tests, plurals
- TaskProgressChip: current-task number is now the first unfinished,
non-cancelled todo's 1-based position (active index + 1), so a completed
task 1 + in-progress task 2 reads as 2/5 instead of 1/5 (review point 2).
- Deterministic resume hydration: hydrate todos directly from cached Room
messages and from the merged REST transcript inside ChatViewModel load
paths, in addition to the WS reducer, so a resumed session rebuilds the
chip without waiting for a follow-up event (review point 3).
- Extracted computeChipDisplay / shouldShowProgressChip pure functions and
added TaskProgressChipTest covering active-task progress, completed/cancelled
hide, subagent-only state, and transcript-resume hydration (review point 4).
- agent count strings are now Android plurals (task_progress_agents /
task_progress_agent_running) in en/zh/ko to avoid '2 agent' (review point 5).
Co-Authored-By trailer intentionally omitted per repo convention.
* fix(chat): derive chip number and content from the same selected task
computeChipDisplay selected the task number from the first unfinished/
non-cancelled todo but the content independently from the first in_progress
todo. With a pending task 1 + in_progress task 2 this produced 'Tasks 1/N ·
<task2 content>'. Now both values come from one selected item (prefer
in_progress, fall back to first pending/non-terminal).
Adds two focused tests: same-item selection, and pending-only fallback.
* build: compileSdk 36 -> 37 + okhttp 5.4.0 -> 5.5.0
Take okhttp 5.5.0 (folds in dependabot PR #940) which hard-requires
compileSdk 37 via AAR metadata. targetSdk stays 36 for now.
* build: targetSdk 36 -> 37 (Android 17) + ACCESS_LOCAL_NETWORK gating
Opt into Android 17 runtime behavior. The app connects to a gateway
(default 127.0.0.1:9119, plus LAN + remote), so targeting 37 forces the
new ACCESS_LOCAL_NETWORK permission for LAN hosts.
- AndroidManifest: declare ACCESS_LOCAL_NETWORK (harmless below API 37)
- LocalNetwork.kt: pure detector + needsLocalNetworkPermission(baseUrl)
(loopback + public remote exempt; RFC1918/link-local/CGNAT gated)
- ConnectViewModel: requestConnect() gates on the permission; screen
launches the prompt; onLanPermissionResult proceeds or shows a clear error
- flake.nix: android platform 36 -> 37
- Tests: LocalNetworkTest + ConnectPermissionGateTest (SDK_INT / permission
branching mocked, no device needed)
- Task 8 finding: assistant services (VoiceInteraction*/Recognition) and the
remoteMessaging ChatNotificationService do NOT hit the Android 17 mic/
while-in-use foreground hardening, so no change required.
* fix: ktlint + @Composable LocalContext hoist (was blocking CI)
- ConnectScreen: hoist LocalContext.current out of onClick lambda
(calling a @Composable inside a non-composable lambda broke compileDebugKotlin)
- ktlint --format: multiline-expression-wrapping, function-signature,
no-empty-first-line-in-class-body, no-unused-imports
- LocalNetwork.kt: merge consecutive KDocs (no-consecutive-comments)
flake.nix buildToolsVersion/platformToolsVersion intentionally stay 36.0.0
— they are decoupled from compileSdk; only platformVersions needs 37.
* fix: suspend mock (coEvery) + flake toolchain pinned to 37.0.0
- ConnectPermissionGateTest: getStatus() is suspend → use coEvery (was
breaking compileDebugUnitTestKotlin)
- flake.nix: buildToolsVersion/platformToolsVersion/cmdLineToolsVersion
pinned to 37.0.0 (verified against pinned nixpkgs 241313f — resolves to
android-sdk-*-37.0.0 derivations; platformVersions already 37)
* fix(flake): revert cmdLineToolsVersion to 11.0 (not API-aligned)
cmdline-tools uses its own versioning (latest = 21.0, never 37.0.0) —
nixpkgs rejects 37.0.0 with 'version missing'. build-tools + platform-tools
stay 37.0.0 (verified resolve to android-sdk-*-37.0.0). flake now evals
clean against pinned nixpkgs 241313f.
* flake: track nixos-unstable + pin toolchain to latest (cmdline 22.0, platform-tools 37.0.1, build-tools 37.0.0, platform 37.1)
- inputs.nixpkgs -> nixos-unstable, flake.lock bumped to rev ffb3c9b7 (2026-08-19)
- cmdLineToolsVersion 11.0 -> 22.0 (own versioning, not API-aligned; v22_0 confirms)
- platformToolsVersion 37.0.0 -> 37.0.1
- buildToolsVersion stays 37.0.0
- platformVersions 37 -> 37.1
- verified: nix flake check --no-build -> all checks passed on hyari
* fix: unit tests — Uri stub + Build.SDK_INT final-field mock
- LocalNetwork.kt: needsLocalNetworkPermission uses java.net.URI instead of
android.net.Uri (latter is a non-functional stub in JVM unit tests, its host
returns null -> gate always false). Added pure requiresLocalNetworkPermission(sdkInt, baseUrl).
- ConnectViewModel: inject sdkVersion (package-private) instead of reading the
final static Build.VERSION.SDK_INT directly; gate via requiresLocalNetworkPermission.
- ConnectPermissionGateTest: drop mockkStatic(Build.VERSION) (MockKException on
final static field, and against repo convention); inject sdkVersion; add pure
gate-decision matrix. Fixes 7 red tests.
* fix(test): remove illegal ':' from test function names (compile error)
* fix(test): order getString mocks so specific resources win over wildcard
* fix(flake): include android-37.0 system image so the emulator can boot
Google only publishes the android-37.0 system image (no 37.1 image exists
yet). With platformVersions=["37.1"] composeAndroidPackages found no matching
system image, leaving system-images/ empty and every AVD FATALing with
'Broken AVD system path'. Adding 37.0 pulls the published image so an AVD boots
(verified: hermes37 AVD boots Android 17 / API 37 on hyari). compileSdk/targetSdk
stay 37.
Also bump AGENTS.md SDK note 36 -> 37 (was stale).
* fix(flake): platformVersions 37.1 -> 37.0
Drop 37.1; 37.0 provides both the SDK platform for compileSdk 37 and the
published android-37.0 system image the emulator needs.
* fix(flake): remove stray tool-scratch line leaked into flake.nix (line 116)
A terminal tool's /tmp scratch rename line got appended to flake.nix, making
it invalid Nix. Removed; flake now parses + 'nix flake check' passes on hyari.
* fix(build): pin buildToolsVersion 37.0.0 to match nix SDK
AGP defaulted build-tools to 36.0.0, which isn't in the nix-managed SDK
(only 37.0.0 is provisioned). Local ./gradlew builds then tried to *install*
36.0.0 into the immutable /nix/store and failed. Pin 37.0.0 so the local
nix devShell builds. CI uses its own SDK so unaffected.
---------
Co-authored-by: Hy4ri <Hy4ri@users.noreply.github.com>
The backend persists a whole class of internal scaffolding as plain
role=user rows with NO display_kind (SessionDB strips underscore metadata):
runtime nudges (max tool-call iterations), context injections ([System:/[CONTEXT/
[PRIOR CONTEXT), background/cron/delegation notices ([IMPORTANT: Background/
Cronjob Response:/[ASYNC DELEGATION), out-of-band markers ([OUT-OF-BAND), and
todo/planning injections ([Your active task list/[Planning state preserved).
These are NOT real human turns, but the mobile rendered them as user bubbles.
Mirror the backend's own synthetic-detection contract
(agent.context_compressor._synthetic_user_row) with a prefix map. Any role=user
row with no display_kind that starts with a known synthetic prefix is routed to
a SystemEvent instead of a user turn. The 90-calls nudge maps to a specific
"Reached tool-call limit" chip; the rest map to a generic "System notice" chip.
Real user messages (no synthetic prefix) are unaffected.
- FullBleedTurns: SYNTHETIC_USER_PREFIX_KINDS map + syntheticSystemKind() that
tags matched rows so the timeline chip can name them.
- timelineMarkerLabelRes: max_iterations_reached + system_notice labels.
- Strings en/zh/ko for both.
- Tests: assert the new labels map; assert every synthetic prefix routes to a
SystemEvent (not a user turn); assert a real user message stays a user turn.
The display_kind-tagged events (model/personality switches, auto-continue,
delegation complete, skill invocation, internal notification) are already
handled separately (PR #936, merged).
ktlint + Android Lint + unit tests green locally.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
System events ride as role=user rows with a display_kind sidecar (backend
issues #904/#82888). The full-bleed renderer routes any displayKind != null
to a SystemEvent, but timelineMarkerLabelRes only had labels for
model_switch / personality_switch / auto_continue / async_delegation_complete.
skill_invocation and internal_notification had NO label, so TimelineMarkerChip
returned early and they fell through to the bare SystemBubble (centered italic
text, no clear system identity) instead of a distinct chip.
- Add skill_invocation + internal_notification to timelineMarkerLabelRes.
- Add string resources en/zh/ko (ko was missing all timeline markers; added
the two new ones for parity).
- TimelineMarkerTest: assert both new kinds map to labels.
Result: every backend system message type now renders as its own centered
timeline chip, never as a user bubble. Legacy sessions persisted before the
backend added display_kind tagging still lack the sidecar and would need a
one-time backend re-tag to stop rendering as user bubbles.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix(sessions): fork icon + depth number, no indentation
Multiply-nested forked sessions rendered with deep indentation that crushed
the card off-screen and made titles unreadable, and the per-sibling order
number was uninformative (mostly "1").
- SessionTreeItem: replace forkIndex (sibling order) with forkDepth (nesting
level: 1 = child of a root, 2 = fork of a fork, ...); flattenSessionTree
populates it.
- SessionCard: render CallSplit fork icon + the depth number (primary tint).
Removed the depth-based start padding so names stay full-width readable —
the icon+number now carries the structure instead of indentation.
- strings: sessions_fork_indicator (en/zh/ko).
- SessionTreeTest: assert isFork/forkDepth across nested fixtures.
No backend change — backend already returns parent_session_id per SessionInfo.
* test(ws): make testAutoReconnect tolerate transient RECONNECTING state
The test gated on connectionStatus.first { it == RECONNECTING } with a
15s timeout. With the test backoff forced to 0ms the RECONNECTING state is
emitted + consumed before the collector attaches, so the wait missed it and
timed out (kotlinx.coroutines.TimeoutCancellationException) — the flake CI
saw under parallel load.
- Replace the brittle RECONNECTING .first{} wait with a tolerant check that
accepts RECONNECTING-or-later (the reconnect clearly fired).
- Keep connect2Latch (second socket opening) as the definitive proof + add a
final CONNECTED assertion.
- Reset the test backoff in a finally so later tests never inherit 0ms.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Backend stamps last_fire_error ({at, detail}) on a job when a scheduled fire
can't reach the gateway loopback (gateway down / api_server not bound) —
the "job runs manually but never auto-fires" case. Previously invisible in
mobile.
- CronJob.kt: model as CronJobFireError(at, detail) — the backend sends a
nested object, NOT a flat string (issue's String? proposal would fail to
deserialize), plus the last_fire_error field.
- CronJobsScreen: "Fire failed" ERROR badge in the list + "Dispatch Error"
row (detail + timestamp) in the Run Details dialog.
- Strings en/zh/ko. Test: job with last_fire_error loads + parses.
Backward-compatible: field has a default, existing jobs without it parse fine.
Co-authored-by: M57 <hy4ri@users.noreply.github.com>