Mobile: live status icons + agent-set status text in the activity card
Status summary#
Implementation done and green locally: reducer derivation + tests, mobile card/feed wiring, and a passing Playwright spec that holds every phase open deterministically. Remaining: PR video, CI, review.
What#
On mobile, while waiting for a response you mostly stare at "writing code…" /
"running code…" next to a generic spinner. Three improvements to the live
activity card (apps/mobile/src/components/activity-card.tsx):
- Second icon next to the spinner, derived from state we already fold:
- nothing — default, when all we know is "queued"/"working"
- pencil — an llm step is streaming response text (writing code)
- play/lightning — a code step is running
- processing — the previous codemode script settled with a returned
value (⇒ the agent owes another LLM round) but the next
llm-request-requestedhasn't landed yet. Only because this IS already derivable from existing events (see mechanics below) — no new events.
- Agent-set status text: when
agent/summary-updated(theitx.agent.append({type: ".../agent/summary-updated", payload: {activity}})call fromAGENT_SUMMARY_INSTRUCTION) has fired during this turn — whether from the first line of the currently running script or from an earlier round of the same turn — show thatactivitytext instead of the generic "writing code…"/"running code…". Fall back to the generic text otherwise. - Round labels: expanded-card round headers say "Round 1 · …" — too verbose. Just "1", "2", "3".
Hard constraint from Misha: no new server-side state or events. Everything derives from the existing journal fold. If that turns out impossible for some sub-state, drop that sub-state (the default "nothing" icon is always allowed).
Existing mechanics (verified before this branch)#
packages/ui/src/components/events/agent-ui-reducer.tsalready foldsagent/summary-updated→state.summaryActivity, and stamps it onto code steps asactivitySummary(running steps immediately, others at settle; new steps inherit it at birth — note that inheritance carries stale, previous-turn text, which is why "set during this turn" needs its own check, see below).SCRIPT_EXECUTION_COMPLETED(capability-host/script-run-settled) recordsresulton the code step only when the settlement carried one (Object.hasOwn(settlement, "result")). Codemode contract: returning a value ⇒ another LLM round follows;return;⇒ turn over. So "processing" = live activity, no running step, last step is a done code step (durable outcome) whose settlement carried a result.- The generic texts live in
liveSummary()in activity-card.tsx; the mobile fold entry point isreduceFeedinapps/mobile/src/lib/feed.ts. - Before this branch
reduceFeedmarked the activity done the moment no step was running — the mid-turn gap (script settled with value → next request) flickered the card to settled and back.
Checklist#
- Phase derivation helper (
deriveAgentUiLiveStatusexported from the reducer:working | waiting | thinking | writing | running | processing+ this-turn statusText; durable-outcome + result-present gate for "processing") - Track "status was set this turn" (new
summaryActivityUpdatedAtMsfold field, compared againstlive.startedAtMs; AgentUiStateSchema is deliberately strict, so old persisted web snapshots invalidate and rebuild — cache data, not a migration surface) - Mobile
reduceFeed: keep the activity live through the processing gap (working ||= phase === "processing"; stuck-spinner guard: the reducer now settles an idle live activity onagent/paused/stream/paused— the autonomous breaker appends agent/paused, so the journal alone ends the gap) - Activity card phase glyph (text glyphs beside the spinner, matching the ApprovalGlyphs pattern: ✎ writing, ▶ running, ↻ processing; aria-labels "writing code"/"running code"/"processing result")
- Activity card status text (this-turn
statusTextbeats the generic fallbacks inliveSummary) - Round header "Round {n}" → "{n}" (activity-card.tsx roundLabel)
- Reducer/derivation unit tests (agent-ui-reducer.test.ts: phase ladder,
processing vs
return;vs failed, this-turn statusText gating, pause settles idle live / keeps running steps live) - Playwright mobile spec (specs/mobile/live-status.spec.ts, passing in ~12s locally: intercepted/status model; "running" pinned by a withTunnel fetch the spec releases; "processing" pinned by llmRequestDebounceMs=4s; round-2 "waiting" pinned by gating the interceptor handler; settled card asserts bare "1"/"2" round headers. The chunk-level "writing" phase is asserted in unit tests only — the interceptor contract returns full text, word-split server-side, so its window can't be held open deterministically from a spec)
Assumptions made (Misha was AFK)#
- "Status associated with this turn" = summary-updated folded at/after the live activity's start; the birth-inherited stale text stays for round headers (existing behavior, untouched) but does NOT replace the live summary text.
- Thinking keeps its existing "thinking…" text and gets no secondary icon (not in the requested list).
- The chat screen's pre-first-step "working…" row keeps no secondary icon (that's the default/queued state).
- Web/TUI feeds are out of scope; the derivation helper lives in packages/ui so they can adopt later.
- Glyphs are text characters (✎ ▶ ↻), not @expo/vector-icons — consistent with the card's existing ◷ ✓ ✗ approval glyphs.
Implementation log#
- Reducer:
summaryActivityUpdatedAtMsfold field;deriveAgentUiLiveStatus; pause events now runsettleActivityAtBoundary(idle live settles, running steps survive — matching the operator-pause-mid-request contract). - Mobile:
reduceFeedexposesliveStatusand counts "processing" as working; chat.tsx passes it to the live card only; card renders glyph + status text; round labels bare numbers. - Spec discovery: the approvals-style isolate warm-up (
runScript) journals onto the agent stream and renders as a stray second activity card — dropped it here (itx-side event waits bridge cold starts) and left a comment. - Lint: middlewright(prefer-locator-waits) rewrote every
expect(locator).toBeVisible()tolocator.waitFor().