Mobile: show chat titles instead of raw stream paths
Status summary: done — PR #2490, CI fully green (preview e2e included),
zero review threads. Server exposes agent titles through
itx.agents.list(), the mobile chat list renders them and stays LIVE off
the agent catalog's push channel, the thread header folds the title from
the event stream. Along the way this exposed and fixed a pre-existing
platform flaw: subscribing to agents.liveState on a fresh project (zero
chats) errored and stayed dead.
Prompted by a screenshot of the mobile chat list showing rows like
mobile/2026-08-12t23-27-59-114z — the raw agent stream path — where a
human-readable title should be. Agents already set a title on their first
turn (agent/summary-updated, per AGENT_SUMMARY_INSTRUCTION), and the web
sidebar shows it; mobile just had no way to read it: itx.agents.list()
returned only path + createdAt.
Checklist#
- Expose
titlefromitx.agents.list()newAgentListItemtype inapps/os/src/domains/agents/agent-presence.ts;list()inrpc-targets.tsmapssummary.titlethrough; regenerated the itx API projections withpnpm generate:itx-api - Chat list rows render the title
apps/mobile/.../index.tsx: title in normal weight, mono path fallback for chats whose agent has not set one yet - Chat list goes LIVE
useLiveState((itx) => itx.agents.liveState)takes over after thelist()first paint — titles pop in on push, and chats started on web/Slack appear without navigation refetches (the spec exposed that returning from a chat showed a stale list) - Thread header renders the title
apps/mobile/.../chat.tsx: folded from the already-loaded event stream via newlatestAgentTitleinlib/chat.ts(same set/clear/preserve semantics asthreadContextForScriptRun); raw path stays reachable via the ••• menu - Platform fix:
agents.liveStateon a fresh project subscribing before the first agent create() was refused withstream-subscription-unconfiguredand the watcher stayed dead (latent in the dashboard sidebar too);AgentCollectionLiveStateRpcTargetinrpc-targets.tsnow appends the idempotency-keyed birth batch on exactly that refusal and retries once - Tests
latestAgentTitleunit cases inapps/mobile/src/lib/chat.test.ts; RN-web specspecs/mobile/chat-titles.spec.ts(signup → new chat → path fallback →/scriptappends the summary fact with ZERO model turns → title takes over header and list); e2elive-state.e2e.test.ts"subscribed before any agent exists" guards the platform fix;specs/mobile/notifications.spec.tsheading assertion updated for the now-titled header
Decisions / assumptions#
- Fixed the product surface rather than having mobile dig into reduced
state ad hoc:
titleis public catalog data, everylist()consumer benefits, and the live list rides the sameuseLiveStateprimitive the dashboard sidebar uses. titleis optional on the wire — absent until the agent's first turn lands — so clients keep a path fallback.- The platform fix is birth-on-refusal (not birth-on-every-subscribe): the already-born common path pays nothing; only the fresh-project refusal triggers the idempotent birth batch append + one retry.
Implementation log#
agents.list()e2e assertions (itx-agents.e2e.test.ts) useobjectContaining/empty-list — tolerant of the added field, no changes needed.- The chat-titles spec failed twice before going green: first on the stale
list after
goBack(fixed by the live list), then on the fresh-project liveState refusal (fixed by the platform change). Verified the push mechanics with a throwaway node script before wiring the UI. - CI's preview lane failed on
notifications.spec.tswaiting for a heading namedelsewhere-thread— that spec seeds a summary title, so the header now (correctly) shows the title instead; assertion updated. - Root-worktree typecheck noise encountered along the way, none of it from
this change: stale expo route typegen + missing
expo-media-libraryin apps/mobile,*ignoreme*scratch files in apps/os.