feat(#527): surface gateway error + background.complete WebSocket events (#550)

Mobile was silently dropping two event types the gateway emits. This adds
parsing + UI surfacing for both, bringing mobile to parity with desktop
(which shows gateway errors as red toasts).

- `error` (backend crash) -> WsEvent.GatewayError, surfaced in the existing
  error banner (mirrors how RpcError is handled).
- `background.complete` (scheduled job finished) -> WsEvent.BackgroundComplete,
  surfaced as a non-blocking snackbar in ChatScreen.

Changes:
- WsEvent.kt: add GatewayError(message?) + BackgroundComplete(data?) sealed subtypes.
- EventParser.kt: handle "error" + "background.complete"; unknowns still fall
  through to Unknown (no behavior change for existing event types).
- ChatWsEventReducer.kt: onGatewayError (sets errorMessage, clears loading) +
  onBackgroundComplete (sets backgroundCompleteMessage); mirrors onRpcError.
- ChatViewModel.kt: backgroundCompleteMessage state field, clearBackgroundComplete(),
  handleWsEvent branches (ViewModel owns the snackbar trigger).
- ChatScreen.kt: pass backgroundCompleteMessage to ChatLifecycleEffects + a
  LaunchedEffect that shows the snackbar and calls clearBackgroundComplete().
- EventParserTest.kt: cover both new events (incl. missing-message case).

Pure additive; sealed class + exhaustive when() preserved. Verified:
compileDebugKotlin + ktlintCheck green; EventParserTest passes.

Closes #527

Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-11 15:18:49 +03:00
1 parent 480d29e481
commit cf893585b1
6 files changed
+160

No files matched your search

@@ -111,6 +111,17 @@ object EventParser {
WsEvent.StatusUpdate(status, payload)
}
"error" -> {
val message =
payload?.get("message") as? String
?: payload?.get("error") as? String
WsEvent.GatewayError(message)
}
"background.complete" -> {
WsEvent.BackgroundComplete(payload)
}
"session.updated" -> {
WsEvent.SessionUpdated(payload)
}
@@ -125,6 +125,33 @@ sealed class WsEvent {
val sessionId: String?,
) : WsEvent()
// ── Gateway-level errors ───────────────────────────────────────────
/**
* Backend/unhandled failure surfaced by the gateway.
*
* Desktop shows these as red toasts (the gateway also writes
* `[gateway.error]` lines to the console). Mobile was previously
* dropping this event, so a crashing turn just silently stopped with no
* explanation. Issue #527 surfaces it in the existing error banner.
*/
data class GatewayError(
val message: String?,
) : WsEvent()
// ── Background job completion ──────────────────────────────────────
/**
* A scheduled/background job finished on the gateway.
*
* Desktop shows a "Background job finished" toast. Mobile surfaces this as
* a non-blocking snackbar. The payload may carry `label`/`name` describing
* the job. Issue #527.
*/
data class BackgroundComplete(
val data: Map<String, Any?>?,
) : WsEvent()
// ── Fallback ─────────────────────────────────────────────────────────
data class Unknown(