HapiProtocol/Window/ — pure port of web/src/lib/message-window-store.ts +
messages.ts, mirroring the Android reference port 1:1:
- MessageWindowState: constants (400/600/800/800/200), MessagePosition,
OlderLoadOutcome, full InternalState fields, persisted v2 snapshot shape
- MessageWindowLogic: every transition (merge/trim preserving queued rows,
latest replace with request-baseline identity preservation, tail sync
begin/apply/finish, older pages + epoch-mismatch reset, enterTailMode
that deliberately keeps requiresLatestReset, activate, SSE ingest with
hidden-row cursor advance, markConsumed stamping server rows to 'sent',
optimistic lifecycle, queued reconcile, hydrate/persist, seededState)
- MessageMerge: position comparator (ASCII tie-break), localId echo
replacement preserving status/invokedAt, 10s sent-dedup fallback
- MessageRetention: calls the fixtures-green chat pipeline's
normalizeDecryptedMessage directly (no hand-mirrored tree to drift)
- WindowMessage: identity-carrying final class (web's !== baseline
classification), tri-state invokedAt, buildOptimisticMessage
HapiClient/Stores/ — async half:
- MessagesProviding seam (three-variant MessagesPageQuery; APIClient
conforms via its existing endpoints)
- MessageWindowController actor: single-flight tail controller with
trailing drain and a synchronous generation bump before the first await
(the web runs to its first suspension; Android used UNDISPATCHED),
fetchOlder with onBeforeApply veto, SSE hooks, optimistic append/status/
cancel-invoked, queued-state reconciliation in 1000-id batches, seedFrom
- WindowSnapshotStore: per-session JSON snapshots, LRU 10 (dedup TODO with
the session-list package's cache) + MessageWindowControllers registry
Tests (swift-testing):
- PaginationFixtureTests: parameterized replay of shared/fixtures/
pagination/*.json against the real controller via a scripted provider;
asserts expectedRequests (canonical JSON incl. explicit-null untils),
expectedOutcome, expectedCandidates, and the final expectedState
projection with per-op labels and first-differing-line diffs
- MessageWindowControllerTests: tail-sync coalescing + trailing drain,
concurrent-SSE preservation across a reset replace, cursor-no-advance
guard, snapshot round-trip/LRU, seedFrom
All 11 fixtures verified green against a line-by-line Python mirror of
these exact algorithms (no local Swift toolchain; CI runs the real suite).
Port web/src/chat/** file-for-file into HapiProtocol/Chat/:
- ChatTypes: block/tool/permission/usage models; AgentEvent as a
record-backed struct (verbatim wire projection) with a typed Kind view
and exact-key-set constructors for every synthesized event shape
- Normalize/NormalizeUser/NormalizeAgent: the full decode tree (codex /
output incl. agy / event families, skip rules, stringify-never-drop)
- Tracer: sidechain grouping via parentToolUseId with prompt-match and
parentUuid-chain fallbacks (orphan reparenting included)
- ReducerEvents (pipe-format limit parsing, dedupe, api-error folding),
ReducerTools (BlockBox shared-mutation model, JS-spread-faithful
permission merging via presentKeys), ReducerCliOutput,
ReducerTimeline (tool pairing, stream coalescing, agent-run cards,
title-changed synthesis, sentinel suppression), Reducer
(pending-permission synthesis gates, latestUsage, goal filtering)
- ToolGroups + codex-exploration family (codexCommandPresentation's
grouping predicates); ToolPresentation (minimal title/subtitle,
full knownTools catalog deferred to HapiUI)
- JSInterop: JS semantics pinned once (nullish coalescing over wire
values, truthiness, safeStringify, canonical JSON serializer with
integer-preserving number formatting, JS-anchored regex helpers)
- FixtureProjection: the normative projection (projection.ts) plus
runChatFixturePipeline
- ChatFixtureTests: one swift-testing case per shared/fixtures/chat/*.json
(48), canonical-JSON byte comparison, per-fixture line-level diff on
mismatch, VERSION gate
Dictionary-order note: wherever the TS relies on JS object insertion
order (agentState requests iteration), the port imposes ascending key
order — equivalent to the reference replay because fixture inputs are
canonically key-sorted on disk and JSON.parse preserves that order.
- HapiProtocol/Pairing/BindLink: parses the companion deeplink
(hapicompanion://bind?hub=&code=) and the web direct-access QR
(?hub=&token=) with URLSearchParams form-decoding semantics, in
lockstep with the Android port (tests mirror BindLinkTest.kt).
- HapiClient/Auth/HubPairingService: normalize -> GET /health
(reachability + protocolVersion) -> POST /api/auth -> persist
Keychain + registry + active hub; unpair with fallback. Covered by
PairingLogicTests through the HTTPPerforming seam.
- App layer: AppModel (@Observable @MainActor pairing state machine:
restore, pair, switch, sign out, deep-link routing, scenePhase,
terminal-auth-failure banner) + HubSession (per-active-hub APIClient/
AuthManager/global SSEClient with suspend-resume and a connection
state for the UI; store routing is TODO(M2)).
- Pairing UI: welcome flow, VisionKit QR scanner (with Simulator/
permission fallbacks), manual entry (paste-friendly), shared confirm
sheet with per-PairingFailure error states.
- HapiApp routes hapicompanion:// through AppModel (paired hubs switch
with a notice, never log the token); RootView switches unpaired/
paired and hosts the deep-link confirm sheet; HomePlaceholderView
shows hub, connection dot, hub switcher (M2a replaces it with the
session list).
- Info.plist: NSCameraUsageDescription; README: pairing guide + manual
test pass.