mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-19 21:09:50 +00:00
Wires the merged M2 pieces into a usable pair -> list -> live chat flow: - pbxproj: link HapiUI into the app target (UUIDs C7/C8, mirroring the existing HapiProtocol/HapiClient product references; only project change). - HubSession: MessageWindowControllers registry + per-chat ChatSession factory with in-memory per-session SSE resume cursors and scene-phase forwarding to the active chat. - ChatSession (app): session-scope SSEClient while the chat is open; window opened/activated before subscribing; a single consume task awaits every event into the window actor, giving Android-channel-equivalent arrival-order ingestion; session-updated and friends route through the shared SyncEventRouter (detail patching), session-removed clears the window, a gap handshake triggers full resync + detail refetch + catch-up tail sync; stop hands the cursor back for seamless reopen. - ChatPipeline (HapiClient): actor running the reduction path off-main - queued-row filter, normalization memoized by row instance identity, reduce + buildVisibleChatBlocks with previousGroups-stable group ids. - ChatModel: window state + detail agentState + machine labels -> serial ~100 ms-coalesced pipeline loop (first run immediate, publishes strictly ordered), header title cascade, loading/empty/error states, loadOlder / retry, last-seen stamping on every update. - ChatView: bottom-anchored ScrollView/LazyVStack (defaultScrollAnchor + scrollPosition(id:)), auto-stick at bottom, new-messages pill, top sentinel paging with documented scroll re-anchoring, degraded banners. - Block views: user bubble w/ attachment chips, agent markdown, collapsed reasoning, tool cards (knownTools.tsx-parity presentation, status chip, read-only permission row w/ pending banner, per-tool bodies: terminal, before/after edits, write content, unified-diff detection -> DiffTextView, checklists, read-only questions, pretty-JSON fallback, children rail), tool groups, centered event rows (presentation.ts port), cli output, generated images (authed bytes + in-memory cache + full-screen viewer), codex review verdict card. - Navigation/links: session list now pushes ChatView; app-level \.hapiOpenURL handler (https/http -> SFSafariViewController, custom schemes confirm first, hapi-file:// -> M4 placeholder) + theme injection. - Tests: ChatPipelineTests drive the runner with fixture-derived windows (stable ids, memo-stable recomputes, queued filter, group-id stability across an older-page arrival). - README: M2f app-layer description.
73 lines
2.4 KiB
Swift
73 lines
2.4 KiB
Swift
import HapiClient
|
|
import HapiUI
|
|
import SwiftUI
|
|
|
|
/// Switches the app root on pairing state and hosts the presentation that
|
|
/// must survive that switch: the deep-link pairing confirm sheet, the
|
|
/// "already paired" notice, the HapiUI palette matching the system
|
|
/// appearance, and the app-wide markdown link handler.
|
|
struct RootView: View {
|
|
@Environment(AppModel.self) private var model
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
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 ?? "")
|
|
}
|
|
.hapiTheme(HapiTheme.resolve(for: colorScheme))
|
|
.handlesHapiLinks()
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RootView()
|
|
.environment(AppModel(
|
|
registry: HubRegistry(defaults: UserDefaults(suiteName: "preview") ?? .standard),
|
|
credentialStore: InMemoryCredentialStore()
|
|
))
|
|
}
|