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>
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>
- New Language row in the main Settings list with current-language summary
- New SettingsLanguage drill-down page with an OutlinedButton + DropdownMenu
picker (scales to any number of languages; same pattern as theme presets)
- Shared supportedLanguages()/languageLabel() helpers — adding a language
is now one entry in one place
- Removed the 4-option segmented pill row from the Appearance page
- Added settings_sec_language string (en + zh)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>