Commit Graph
268 Commits
Author SHA1 Message Date
M57 cdcecb9734 feat(profiles): add local on-device profile hiding and filtering 2026-08-30 20:51:06 +03:00
M57andM57 97b6641d84 feat(bots): emergency stop button and /stop slash command in group chat (#1012)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-30 00:02:16 +03:00
M57andM57 fbe602cb4f feat(bots): lightweight tool execution indicators in group chat (#1008)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-29 19:11:14 +03:00
M57andM57 0d09f43469 feat(bots): reactive multi-agent group chat orchestration with CAS leasing (#1007)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-29 18:44:08 +03:00
M57andM57 7e24f5eba3 fix(bots): sync cross-device group chats and roster from desktop projection (#1006)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-29 13:56:40 +03:00
M57andM57 5bc6e5f855 feat(settings): in-app chat font size setting (#1004) (#1005)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-29 11:36:56 +03:00
M57andM57 db3f8c736e feat(markdown): support variable-length code fences and nested codeblocks (#1003)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-28 20:27:17 +03:00
M57andM57 7a2c453f58 fix(history): guard fluid pagination against infinite network loop on small session lists (#1002)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-28 19:44:28 +03:00
M57andM57 2332bdd5d5 feat(nav): root swipe-back on chat with double-back exit and subpage hierarchy (#1001)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-28 18:38:01 +03:00
M57andM57 f04d878358 feat(bots): add multi-agent group chat collaboration room (#998)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-28 17:50:37 +03:00
M57 0cdccad855 fix(bots): create clean profile without cloning default soul and custom skills (#997) 2026-08-27 19:06:58 +03:00
M57andM57 4d9dfb99db feat(db): migrate to androidx.room3 3.0.2 with SQLCipherDriver (#994)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-27 13:47:32 +03:00
AlexDev_ 0ae5ed4db2 feat(chat): stabilize multi-image attachment picker (#986) 2026-08-27 10:50:55 +03:00
AlexDev_ c7d7abf4a1 fix(chat): prevent timeline prefetch ANRs (#985) 2026-08-27 10:47:04 +03:00
AlexDev_andM57 ce8fdc45bd fix(notification): open tapped chat at bottom (#984)
Co-authored-by: M57 <41645758+Hy4ri@users.noreply.github.com>
2026-08-27 10:41:08 +03:00
M57 2fed891d78 feat(ui): add Bots and Groups tab strip with GroupCard and disband action (#987) 2026-08-27 01:53:54 +03:00
M57 6ca248d28b feat(ui): add EditBotBottomSheet with avatar editor and bot deletion (#983) 2026-08-27 01:53:15 +03:00
M57 274120cc86 feat(ui): add CreateGroupChatDialog for multi-bot room creation (#982) 2026-08-27 01:52:34 +03:00
M57 d4d6a04afc feat(ui): add CreateBotDialog to BotsScreen with avatar customizer (#981) 2026-08-27 01:51:43 +03:00
M57 b422ead921 feat(chat): add @bot mention autocomplete to ChatComposer (#980) 2026-08-27 01:51:10 +03:00
M57 4e666b49a8 feat(chat): intercept /new in canonical Bot Chat sessions to /compact (#979) 2026-08-27 01:50:43 +03:00
M57 48c9039e02 feat(ui): add BotsScreen and BotsViewModel with active presence strip (#977) 2026-08-27 01:49:31 +03:00
M57 6af349cdf2 feat(ui): add reusable Material3 BotAvatar component (#974) 2026-08-27 01:49:08 +03:00
M57 c028077647 feat(models): support bot metadata, canonical sessions, and group sync in Profile models (#973) 2026-08-27 01:48:43 +03:00
M57andM57 1f1ebf3597 feat(update): redesign in-app updater with smart cooldown, release notes, and in-place dialog (#971)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-26 07:42:41 +03:00
M57andM57 460472f631 fix(chat): queue prompts while session creation is in flight (issue #969) (#970)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-26 06:35:44 +03:00
M57andM57 14b9c1d1d3 fix(chat): support multi-level nested bullet, task, and ordered markdown lists (#965) (#968)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-25 17:41:47 +03:00
M57andM57 9217f9f8b6 fix(sessions): tolerate integer and string boolean formats for pinned field (#966) (#967)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-25 17:05:14 +03:00
M57 ece0288ca8 feat(update): surface backend update receipt in action popup (#958) (#964)
Adopt GET /api/hermes/update/receipt and append its summary to the
end of the hermes-update action progress popup after it finishes.
Additive only: existing checkHermesUpdate + connection-retry polling
is untouched. 404 (old backend) is handled gracefully.

- add UpdateReceiptResponse data model matching backend schema
- add getUpdateReceipt() to HermesApiService
- ActionProgressController gains trailingLines + pushTrailingLines
- ActionProgressDialog renders trailingLines after the log
- SystemViewModel fetches receipt on action finish and formats it
- extract pure formatUpdateReceiptLines for mockk-free testing
2026-08-24 01:35:59 +03:00
M57andM57 5fd732be9b fix(chat): preserve ordered list numbering in loose lists (#959) (#963)
ORDERED_RE only captured item text and discarded the source number, and the
parse loop broke on the first blank line — so loose numbered lists (blank lines
between items) re-indexed every item to 1.

- capture the prefix number and emit it as MdBlock.Ordered.index
- treat a blank line as list-internal when the next non-blank line is another
  ordered item, so loose lists stay one ordered list
- keep sequential numbering for tight lists (1,2,3) and honor custom starts

add regression tests in MarkdownTextFeatureTest (loose, custom-start, tight).

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-24 00:16:27 +03:00
M57andM57 764cb3455f fix(auth): await cookie scope hydration on cold start (#961) (#962)
* fix(auth): await cookie scope hydration before ticket mint and request loading (#961)

* refactor(auth): scope latch activation to active store hydration

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-23 23:57:37 +03:00
M57andhy4ri bb400881e6 ws: harden HermesWsClient (stale-socket watchdog, JSON ticket parse, dead-branch removal) (#954)
* ws: watchdog cancels stale sockets to trigger reconnect

* ws: parse ws-ticket response with kotlinx.serialization

* ws: remove unreachable buffer-overflow warning

* style: ktlint format ws client tests

---------

Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
2026-08-22 18:49:50 +03:00
M57 5153e1572f fix(chat): dedupe user bubbles enriched with attachment ref lines (#952)
Sending an image/file renders the user message twice: the optimistic
bubble carries the raw caption while the backend persists an enriched
copy with "@image:" / "@file:" ref lines and a "[screenshot]" marker
(_build_image_ref_message / run_agent multipart flattening). The trimmed
exact-content compare in sameLogicalMessage fails on that drift and the
prefix fallback gate (>=40 chars) rejects short captions, so the REST
transcript sync appends the REST copy as a second bubble.

Compare USER rows on the caption with attachment-injection lines
stripped (stripAttachmentRefLines). USER-only so assistant text that
legitimately mentions these tokens is never collapsed.

Device-verified on the hermes37 emulator: image send + transcript
resync renders a single user bubble, restart/reload stays clean.
Unit: ChatToolDedupeTest 32/32.
2026-08-22 12:33:48 +03:00
M57 4549410a45 chore(ui): replace emoji with Material icons across chat UI (#944)
* 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.
2026-08-21 00:07:37 +03:00
M57 fad49b7b66 feat(chat): compact sticky task progress chip (#942) (#943)
* 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.
2026-08-21 00:03:50 +03:00
M57andHy4ri 59bcbdfe3c build: Android 17 (API 37) + okhttp 5.5.0 + LAN local-network permission gate (#941)
* 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>
2026-08-20 14:35:42 +03:00
AlexDev_ bb4ca6bfa9 feat(chat): render LaTeX formulas natively (#939)
* feat(chat): render LaTeX formulas

Support native inline and display math with accessible Compose rendering.

* perf(chat): reuse LaTeX parsing state

Share one measurer per message and skip redundant math splitting.
2026-08-19 22:41:50 +03:00
M57andM57 caed4db316 fix(chat): route ALL synthetic backend user rows to system events (#937)
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>
2026-08-18 19:04:44 +03:00
M57andM57 0d1b4fee11 fix(chat): give every backend-tagged system event its own timeline chip (#936)
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>
2026-08-18 17:32:26 +03:00
M57andM57 82e2d787af fix(sessions): show fork icon + order number on nested forked sessions (#934)
* 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>
2026-08-18 17:06:51 +03:00
M57andM57 79c2ea1341 feat(cron): surface last_fire_error on Cron Job records (#925) (#933)
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>
2026-08-18 12:53:36 +03:00
M57andM57 1039d7db1d feat(cron): add Run Continuity toggle + badge (#924) (#932)
Expose backend cron self-continuity (context_from=["self"]) in the Cron
editor. Adds a Run Continuity checkbox to the job editor that sends
context_from=["self"] on create and update (null when off, clearing it),
prefills from the loaded job's context_from, and shows a "Continuity" badge
on recurring jobs in the list. context_from was already on CronJob and
CreateCronJobRequest, so this is editor/list wiring only. Strings added for
en/zh/ko. Includes two ViewModel tests.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-18 12:36:05 +03:00
M57andM57 50a0f30d8f feat(#920): adopt GET /api/files/stream for seekable audio and video playback (#930)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-18 12:02:36 +03:00
M57andM57 acd82c0085 feat(#919): adopt session.usage WS push event for real-time context & compression updates (#927)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-17 23:31:07 +03:00
M57andM57 d6892cde7f fix(#918): fix profile clone endpoint and prune dead MCP restart & session prompt REST calls (#926)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-17 22:51:29 +03:00
M57andM57 75023a2bf0 feat: show a loading indicator on gateway attachments while they open (#917)
Tapping a gateway attachment gave zero feedback while the file
downloaded (a 100MB file could sit there silently for tens of seconds).
Mirror the save spinner: ChatUiState gains openingAttachmentPath, set
when openAttachment starts the download and cleared in a finally, and
the attachment card renders a small CircularProgressIndicator while its
path is being fetched. Wired through FullBleedChatList, ChatBubble and
FullBleedAgentMessage.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 23:28:04 +03:00
M57andM57 d8be45b8fc fix: reuse cached gateway downloads within a 10-minute TTL (#915)
Every tap on a gateway attachment re-downloaded the whole file: the
cache write used a random UUID prefix, so no two fetches ever produced
the same filename and old copies were never looked up.

Cache files are now named deterministically from the gateway path
(UUID3 hash + filename), so repeat opens within the TTL (10 min) reuse
the existing download and skip the network entirely. Stale entries fall
through to a fresh fetch that overwrites the cache via temp-file +
rename, so a failed re-download never destroys a previously-good copy.
Content types are remembered in-memory per path so cache hits still
hand the viewer the right MIME.

Also adds isFreshCacheFile()/cacheFileNameFor() unit tests.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 23:05:10 +03:00
M57andM57 b653903fa8 fix: stream gateway downloads to disk instead of slurping into RAM (#914)
* fix: stream gateway downloads to disk instead of slurping into RAM

Opening or saving a ~100MB MEDIA file OOMs the app: fetch() read the
whole response body into a single ByteArray (peak heap ~2x file size)
before writing it to cache. A 102MB docx (~97MiB) passed the server's
100MiB cap but blew the phone heap (192MiB growth limit) on both open
and save.

fetch() now streams the body to a cache file in 64KiB chunks and
GatewayFile carries the written file instead of bytes; save copies the
cache file to the destination chunked. Cancellation-safe (partial file
deleted, CancellationException never swallowed), UUID-named cache
files, 24h sweep.

Closes #913

* fix: stub app.cacheDir/applicationContext in attachment tests

The streaming fetch builds File(cacheDir, ...) from the application
context's cacheDir; the affected tests never stubbed it, so the mock
returned a bogus File (null path) and java.io.File's constructor NPE'd
before fetch was even called. Stub cacheDir + applicationContext
(returning app itself, matching the existing success-path test) in the
five open/save tests. copyChunked is also stubbed to copy for real in
the save-writes test since mockkObject mocks the whole object.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 23:05:10 +03:00
M57andM57 4be1e965f2 feat: render display_kind timeline markers as chips, not user bubbles (#904) (#909)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 14:06:51 +03:00
M57andM57 85087c70c7 feat: surface /api/status memory+disk pressure on connect/status screens (#903) (#908)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 13:00:08 +03:00