mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-14 20:26:05 +00:00
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.
68 lines
2.1 KiB
Swift
68 lines
2.1 KiB
Swift
import HapiClient
|
|
import SwiftUI
|
|
|
|
/// Switches the app root on pairing state and hosts the presentation that
|
|
/// must survive that switch: the deep-link pairing confirm sheet and the
|
|
/// "already paired" notice.
|
|
struct RootView: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
@Bindable var model = model
|
|
Group {
|
|
switch model.state {
|
|
case .unpaired:
|
|
PairingFlowView()
|
|
case .paired:
|
|
if let session = model.session {
|
|
// `.id` resets navigation + list state on hub switch —
|
|
// each hub gets a fresh HomeView over its own stores.
|
|
HomeView(session: session)
|
|
.id(session.hubUrl)
|
|
} else {
|
|
// Defensive: .paired always carries a session; fall back
|
|
// to pairing rather than a dead screen.
|
|
PairingFlowView()
|
|
}
|
|
}
|
|
}
|
|
.sheet(item: $model.pendingPairing) { pending in
|
|
NavigationStack {
|
|
PairingConfirmView(pending: pending)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") {
|
|
model.pendingPairing = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.alert(
|
|
"Hub already paired",
|
|
isPresented: Binding(
|
|
get: { model.infoNotice != nil },
|
|
set: { presented in
|
|
if !presented {
|
|
model.infoNotice = nil
|
|
}
|
|
}
|
|
)
|
|
) {
|
|
Button("OK") {
|
|
model.infoNotice = nil
|
|
}
|
|
} message: {
|
|
Text(model.infoNotice ?? "")
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RootView()
|
|
.environment(AppModel(
|
|
registry: HubRegistry(defaults: UserDefaults(suiteName: "preview") ?? .standard),
|
|
credentialStore: InMemoryCredentialStore()
|
|
))
|
|
}
|