The hub's fork, rewind and export routes had no iOS UI. The chat's
menu now offers:
- Fork Conversation: when the agent advertises forkCurrent, the
session is live and no Codex turn is running (the web gate). The new
session opens in place of this chat.
- Rewind or Fork…: a sheet listing loaded user messages that have a
native point in metadata.conversationHistoryPoints. Each row offers a
confirmed Fork from Here or Rewind to Here, gated like web: forks
need an idle turn except on Codex, and rewinds always need one. The
actions live in a sheet, not on the transcript bubbles, because those
keep text selection and cached row heights. After a rewind the tail
resyncs with a history reset. An ambiguous native boundary (409
ambiguous_native_boundary_fork_safe) offers a fork from the same
point instead.
- Export Conversation…: saves the hub's lossless JSON payload through
the Files save panel, using the web download's filename. A large
export asks before retrying with force; a 413 explains the limit.
Markdown export stays web-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS showed scheduled messages but could not create them. A clock
button now sits next to the composer's attachment button. It offers
the web presets (in 5 min, 30 min, 1 h, 4 h) or a specific time up to 7
days ahead. A chip above the input shows the pending time; its × sends
normally again.
Presets resolve when the user taps Send, so "in 5 minutes" counts from
the send. Validation follows the web: a 30 s grace period for past
times and a 7-day maximum.
A scheduled send is text-only and queued. The hub rejects scheduledAt
with attachments or steer, so the clock is disabled while attachments
are present and the steer menu is hidden. The schedule clears once the
send dispatches. Sending reuses the existing optimistic path, and a
retry keeps the row's scheduledAt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typing / in the iOS composer offered nothing; web and Android show the
session's commands. While the input is a lone /token, a list above the
card shows matching commands. A tap inserts "/name " so the user can
add arguments.
As on Android, the sources are metadata.slashCommands plus the
/slash-commands RPC. The RPC is fetched once, when the input first
enters slash mode, and its entries override bare names. A failed fetch
retries on the next /. There is no copy of the shared built-in table,
so the CLI stays the source of truth. Ranking follows web: exact,
prefix, contains, then near-miss typos. The list is hidden in
Scratchlist mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The settings sheet only had model pickers for Claude and Codex; other
agents could not change models after creation. Live sessions now load
/sessions/:id/<flavor>-models. Agy uses its machine catalog, since it
has no session route. As on web, grok and copilot skip
terminal-controlled sessions.
Rows follow web getModelOptionsForFlavor:
- Copilot, dsh, grok and kimi offer a Default or Auto row that clears
the model.
- Cursor's Auto is the explicit 'auto' wire id.
- Opencode, agy and pi only switch between listed models.
- An unlisted current model stays visible.
Pi resolves provider/modelId from metadata.piSelectedModel and switches
with a {provider, modelId} reference.
Efforts:
- Grok and OpenCode options come from their *-reasoning-effort-options
routes and reload after a model switch. OpenCode options that still
describe the previous model stay hidden.
- Pi shows the current model's thinking levels.
The sheet's load-failure flag now covers every catalog, not just Codex.
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>
Add current-proposal decoding, dedicated implementation requests and composer-only continuation on iOS and Android. Keep pending/errors across row recycling, reject stale actions and prevent duplicate submissions.
Include native regression tests and localized plan controls.
Add viewport-driven paging with layout acknowledgements, bounded retries, cancellation gates, and epoch-safe history retention.
Preserve transcript anchors and expansion state, fix tool-group identity collisions, and serialize Android history coordination on Main.
Reduce per-scroll composition and layout work; add native regression tests, CI coverage, and profiling guidance.
Constrain transcript rows to the viewport and replace competing scroll anchors with measured tail corrections. Preserve following through media resizing and reset scroll state when the transcript remounts.
Gate history paging on reader gestures and cover tail-following behavior with seven regression tests.
ios/scripts/linux-test.sh stages HapiKit + shared/fixtures at repo depth
into a persistent tmp dir and runs swift test in swift:6.1-noble; the
manifest drops HapiUI (SwiftUI/swift-markdown/Highlightr) under
#if os(Linux). 458 tests green, including all 48 chat and 11 pagination
golden fixtures.
Fixes that fell out of the first real compile of the blind-written port:
- ChatTypes.CodexReview.wireValue + SummaryPatching legacy id fallback:
split expressions that exceeded the Swift 6 type-checker budget
- FileEndpointsTests: raw string containing "# terminated the literal
early (never compiled anywhere) — now ##-delimited
- SSEClient.backoffSleep: Task { try? ... } inferred Task<()?, Never>
- Darwin gates: Security/Keychain behind canImport(Security) (tests use
InMemoryCredentialStore via CredentialStoring), CryptoKit digest with
FNV-1a filename fallback, FoundationNetworking imports for URLSession
types, corelibs URLCache diskPath: initializer, get-only
waitsForConnectivity, delegate-based SSE transport where
URLSession.bytes(for:) does not exist
Per-session parked notes mirroring the Android B-M4d feature:
- HapiProtocol Models/ScratchlistApi.swift: wire types for entries CRUD
(idempotent create via client entryId+createdAt), attachment metadata,
limits (defaults from shared/src/scratchlistAttachments.ts), upload
envelope, and the typed error codes (scratchlist_at_cap,
scratchlist_attachment_too_large, scratchlist_attachment_in_use, ...).
- Endpoints/ScratchlistEndpoints.swift: GET/POST/PUT/DELETE entries,
limits, base64 upload, raw-bytes attachment fetch, attachment delete.
- Stores/ScratchlistStore.swift: @MainActor @Observable per-session cache
behind the SessionScratchlistStoring seam - open/release observation,
16 ms-coalesced refetch on the scratchlistUpdatedAt SSE signal,
optimistic create/update/delete with surgical entryId reconcile +
rollback (refresh preserves in-flight optimistic creates), 200-entry cap
pre-check + hub 409 verdict, uploadsInFlight, cached limits with
offline defaults, UTF-16 text clamp at 10000.
- Stores/ScratchlistAttachmentGuard.swift: pure Fits/Downscale/Reject
budget verdicts ported verbatim.
- SessionListStore: onScratchlistInvalidation callback fired when a
session patch carries scratchlistUpdatedAt (the seam the M4b comment
reserved); HubSession wires it into the store and injects the store
into ChatInteractor.
- ChatInteractor (additive tail section): scratchlist store property,
scratchlistCount badge seam, insertComposerText, parkComposerDraft
(composer clears only after the hub accepts; at-cap/failed keep the
draft).
- Features/Scratchlist/: sheet off the chat toolbar's note icon with
count badge - entry cards (4-line preview, relative age, authed
thumbnails via NSCache loader, filename chips), edit sheet
(PhotosPicker -> guard -> JPEG downscale -> upload spinner tile,
remove, delete/save), full-screen viewer (GeneratedImage pattern),
per-entry To composer, and Park current draft in the screen header (a
deliberate placement divergence from Android's composer button - the
composer UI is owned by the concurrent attachments package). iOS-only
import step transcodes disallowed-but-decodable rasters (HEIC) to JPEG
before the guard.
- Tests (36, all transcribed from or mirroring the Android suites): store
CRUD optimistic/rollback/at-cap/invalidation/upload/limits with
canonical wire-body asserts + the SessionListStore seam test, 9 guard
verdicts, 9 interactor park/insert/badge seam tests over a fake store.
Verified on Linux via a dockerized Swift 6.0 scratch copy (FIFO HTTP
performer): full HapiClient build under strict concurrency, 36/36 new
tests green, SessionListStore 17/17 and ChatInteractor 29/29 suites
green; app-side screen model + loader typechecked against stub
frameworks; SwiftUI views and the CG-based import parse-checked (macOS
CI compiles them).
Interaction layer turning the read-only chat into a working remote control,
mirroring the merged Android B-M3ab feature-for-feature (web authority where
the ports disagreed):
- ChatInteractor (HapiClient, fully swift-test covered): optimistic composer
sends (appendOptimistic -> POST -> status settle), queue-by-default with a
long-press Send&Steer intent while a turn is active, tap-to-retry on failed
rows (steer retries degrade to queue), per-session drafts
(UserDefaultsChatDrafts, hub-scoped keys, debounced + flushed on close).
- session_inactive (409) recovery: one POST /resume (current permissionMode)
then retry; a superseding session id seeds the new window
(MessageWindowControllers.seed), migrates the draft, retargets the
optimistic row and emits sessionSuperseded -- ChatView/HomeView replace the
navigation entry in place.
- Queued bar: uninvoked sends in web sort order with Cancel (optimistic
DELETE; invoked-race ingests the authoritative row as sent), Edit
(cancel + composer prefill, newer-draft guard) and Steer (invoked answers
reconcile a missed consume); single-flight per-row op guard.
reconcileQueuedState now runs on chat open and on session-pipe gap.
- Permission actions: flavor-exact bodies mirroring PermissionFooter.tsx --
claude {} / allowTools (Bash(cmd)) / mode:acceptEdits, codex-family
decision approved / approved_for_session / abort via deny -- plus
AskUserQuestion flat answers (option cards, Other free text, no-questions
fallback, cursor stable ids) and request_user_input nested answers
(user_note suffix, required validation); optimistic Resolving /
AlreadyHandled (404/409) overrides settled by the agentState patch.
ChatPipeline now re-attaches the window row's client status so failed
user rows actually render the retry affordance (web normalize.ts parity;
the Android reference misses this overlay).
- Session config sheet (toolbar gear): catalog-driven permission-mode picker
with tones, claude static model/effort catalogs (ModelCatalog port), codex
models via new GET /sessions/:id/codex-models endpoint + wire types with
per-model reasoning efforts; optimistic detail updates
(SessionListStore.updateDetailLocal, new) rolled forward to server truth
on error.
- Lifecycle: VisibilityReporter posts POST /api/visibility per tracked
handshake subscriptionId on scene-phase flips (404 prunes); ChatSession
exposes its subscriptionId and feeds the reporter; the global SSE pipe was
already HubSession-lifetime on iOS.
- Tests: ChatInteractorTests transcribes the Android interaction suite
against the real APIClient/AuthManager/SessionListStore/window registry
with only HTTP scripted -- canonical JSON bodies asserted byte-for-byte
(send/approve/deny/config), optimistic send happy/fail/retry, 409 resume
both id paths, cancel invoked-race, steer reconcile, edit prefill,
override lifecycle, config optimistic + rollback, drafts, abort.