- Transient SSE states appear only after 2s, as the iOS 26 nav subtitle
(a gray line with a spinner where no title is shown), so the list
never shifts on routine reconnects
- Only a failed list refresh is amber: "Offline · updated …" with Retry
Running sessions previously suppressed the ordinary unread dot, hiding
answers the user had not seen. Unread is now
manualUnread || contentRevision > readRevision on web, iOS and Android.
Web rows draw the running/background spinner beside the unread dot, and
unread outranks background status. Because session patches arrive before
the message they count, a chat following its visible tail hides its own
ordinary dot until the rendered acknowledgement lands (display-only; web
and iPad split view).
- Large collapsing title; rename the second section to Recent
- Swipe actions: pin/mark read (leading), archive or delete/reopen (trailing)
- Two-line agent icon with a presence badge (pulses while running) instead
of dimming disconnected rows and per-row spinners
- Larger timestamps, compact section headers, glass-friendly filter icon
- Drop the pixel-level row presentation tests
Track readable content revisions and durable read cursors in the hub. Acknowledge only rendered foreground tails, persist offline read operations, and preserve newer manual unread choices across devices.
Migrate existing history to a read baseline and cover pagination receipts with shared native conformance fixtures.
The iOS session list could only filter by machine. Web also has text
search, an unread-only filter and Mark All as Read.
- Search: a .searchable field ranks matches with the web relevance
rules, ported from sessionListSearch.ts via Android as
SessionListSearch. It searches title, worktree, summary, agent,
machine, path and id; every word must match, with */? wildcards.
Results keep the global/project pin sections, so the Pinned header
still applies.
- Filters menu:
- It now holds an Unread Only toggle that keeps the iPad's selected
chat listed.
- The machine section appears only with two or more machines, as
before; the menu itself shows whenever the hub has sessions.
- Mark All as Read advances the local last-seen watermarks after a
confirmation, as on web.
- Empty list: an empty search or filter result gets its own empty state
instead of "No sessions yet".
getSessionTitle moves into HapiKit so search and the rows share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API wrappers existed but iOS had no UI for these actions, while
web and Android did. The session row's context menu and the chat's …
menu now offer Rename for every session. Active sessions keep Archive.
Inactive sessions get Reopen and a confirmed Delete instead of
Archive, which the hub rejects with 409 for them.
The store applies renames and deletes optimistically. A failed rename
refetches server state; a failed delete restores the row and the
cached detail. Reopen marks the returned session active right away.
When reopen returns a superseding id, the chat seeds the new window,
moves the draft and navigates to it, as send-resume already does. A
reopen from the list also moves the draft and opens the session. A
422 shows the hub's message with the missing fields.
On iPhone, an authoritative removal (delete, or archive elsewhere) now
pops that chat, matching the iPad split, which already clears the
selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows carried a machine-only line plus the full absolute path (prefix
noise, tail-truncated exactly where the information lives, machine
repeated under the filter chips). Replace both with a single meta line:
the last two segments of the worktree base path (session path fallback
— the web sidebar's group-name rule), the worktree name when present,
and the machine label only while several machines are known with no
machine filter active. The subtitle now carries the AI summary or
nothing; full paths stay in the session detail. Typical rows shrink
from three or four lines to two.