weishu
67b7fe7304
feat(ios): session list store + UI (A-M2a)
...
HapiProtocol (pure, mirroring the Android reference port):
- SummaryPatching: sessionSummary.ts derivations (pending requests
cap-5/oldest-first/id-tiebreak, kinds, todo progress), toSessionSummary
projection with the per-flavor agentSessionId resolution (legacy chain
omits piSessionId, replicated), applySessionSummaryPatch with the
deliberate >= version gates (vs the detail path's strict >), the
keep-alive render-irrelevance filter (pendingRequests compare ignores
'since'; metadata compare ignores hapiMcpUrl), max-monotonic updatedAt,
post-patch updatedAt as fallback 'since', and the deprecated legacy
detail-required gate kept for rule pinning.
- SessionSorting: exact list comparator (globalPinned > pinned > active >
pendingRequestsCount desc among active > updatedAt desc; stable).
- SessionMetadata: per-flavor session-id fields (claude/codex/gemini/
opencode/grok/agy/cursor/kimi/copilot/pi).
HapiClient stores (@MainActor @Observable, per hub):
- DiskCache: 500 ms debounced atomic JSON snapshots + SnapshotLocations.
- SessionListStore: sorted summaries + detail cache; full-session upsert
preserving hub-computed scheduled fields; strict-> detail vs >= summary
patch paths; keep-alive identity preservation (listRevision); empty-{}
and unparseable payloads take the REST fallback; 16 ms coalesced
refresh; optimistic pin (roll-forward) and archive (restore).
- MachineStore: the exact machine-updated decision tree.
- LastSeenStore: monotonic unread watermarks + per-scope baseline.
- SyncEventRouter: SyncEvent fan-out to the stores, global-scope message
events refresh the list, gap handshake triggers the full resync.
App:
- HubSession owns the stores + router (replaces the TODO(M2) routing);
background flushes snapshots.
- Features/Sessions: SessionListView + SessionListModel (status dot with
thinking pulse, title cascade, flavor·machine·worktree meta, relative
age on a minute timeline, pending/todo badges, unread dots, pinned
section, machine filter chips >= 2, pull-to-refresh, empty/loading/
offline states, long-press pin/archive context menu) + an M2f chat
placeholder pushed on row tap.
- HomePlaceholderView -> HomeView hosting the list (hub switcher +
connection dot kept in the toolbar).
Tests transcribe the Android suites: SummaryPatchingTest (18),
SessionSortingTest (6), SessionStoreTest, MachineStoreTest,
LastSeenStoreTest, JsonSnapshotStoreTest, StoreSyncTargetsTest — using
the existing HTTPPerforming recording stub plus a path-routing performer
for concurrent refetches.
2026-08-17 20:17:02 +08:00
weishu
3363d1c75b
feat(ios): pairing flow, deep link, app session wiring (A-M1d)
...
- 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.
2026-08-17 15:58:17 +08:00
weishu
9d2ef4e01c
merge: A-M1c iOS SSE client + reconnect state machine
...
# Conflicts:
# ios/README.md
2026-08-17 15:29:20 +08:00
weishu
15f48c9c65
feat(ios): SSE client + reconnect state machine (A-M1c)
...
HapiClient/SSE per docs/api/client-contract/sse.md and the web reference
(web/src/hooks/useSSE.ts):
- SSELineParser: incremental frame parser (LF/CRLF, multi-line data,
comments, retry ignored); per-block ids only — sticky-cursor semantics
live in the caller so id-less heartbeats can never blank the cursor.
- ReconnectPolicy + SSETimings: 0s first retry, 1s x2 capped at 30s,
300s slow ceiling after 8 attempts, 0-500ms injectable jitter; connect
timeout 10s, staleness 90s, foreground-resume 45s, watchdog tick 10s.
- SSETransport protocol + URLSessionSSETransport (URLSession.bytes with
dedicated config: 300s idle timeout, 7d resource, no waitsForConnectivity).
- actor SSEClient: idle/connecting/connected/backoff/suspended machine;
handshake-gated connected state surfacing resume ok|gap; per-subscription
cursor sent as ?lastEventId, advanced only after consumer yield
(at-least-once); one free token-refresh retry per connect cycle on 401;
suspend defers retries, resume applies the 45s staleness check;
acceptEncodingIdentity escape hatch (gzip streaming verification TODO).
- NetworkPathObserving + NWPathObserver: path change while connected is
treated as a transport error (immediate reconnect).
- swift-testing suite on a fake transport + manual clock: parser edge
cases, exact backoff schedule + seeded jitter bounds, handshake gating,
ok/gap verdicts, cursor replay + isolation, heartbeat vs watchdog,
90s staleness, connect timeout, suspend/resume, event ordering,
unknown-type passthrough, 401 bypass cap, path-change reconnect.
2026-08-17 15:28:14 +08:00
weishu
1f0ec6184d
feat(ios): HapiClient API transport + auth (A-M1b)
...
APIClient (final class, Sendable) with typed endpoints for the M2/M3 REST
surface behind an HTTPPerforming seam; actor AuthManager with single-flight
JWT refresh (POST /api/auth), proactive refresh 10 min before exp, terminal
authFailed state, and 401 -> refresh -> retry-once wiring per
docs/api/client-contract/auth.md; Keychain credential store (per-hub records
under run.hapi.companion) with CredentialStoring seam + in-memory double;
HubRegistry (ordered hubs + active hub in injectable UserDefaults, origin
normalization); payload-only JWT decoding; APIError {status, code, body}
parsing per errors.md; minimal multipart builder for M4c dictation; 256 MB
URLCache session for generated images.
Request/response wire models (SendMessageRequest, PermissionApproveRequest,
SpawnRequest/SpawnResponse, MessagesQuery, envelopes, RPC-wrapped shapes)
join HapiProtocol/Models mirroring shared/src/apiTypes.ts.
swift-testing coverage: JWT decode (padding/garbage/hostile exp), auth
single-flight (8 concurrent callers -> one exchange), 401 retry-once and
terminal paths, APIError body parsing, endpoint request construction
byte-checks (cursor queries, deliveryMode body, answers formats, explicit
null model reset) via a recording performer, hub URL normalization,
multipart bytes.
2026-08-17 15:20:03 +08:00
weishu
e0e7be39c3
feat(ios): scaffold SwiftUI app + HapiKit package + CI (A-M0)
2026-08-17 11:18:50 +08:00