#40: BridgeNavigationDelegate gains onDidFail closure (covers didFailProvisionalNavigation + didFail). WebviewBridge.failHandshake(reason:) transitions to new terminal .loadFailed state, cancels pending 2s timeout, fires existing alertPresenter, idempotent. AppDelegate.handleWebviewLoadFailure dual-surfaces: failHandshake + critical non-dismissible banner (defense-in-depth across test seams). WebviewBootHealthProbe gets .loadFailed case. #41: HudStateCoordinator emit closure now ALSO calls menuBarController.transition(to:) in addition to bridge.send(.hudState(...)). Doc-comment correctly notes this is not a HUD-08 single-writer violation — transition writes currentState, not BusOutbound.hudState. scripts/check-single-writer-hudstate.sh still green. Tests: 4 new Handshake cases (load-failure-from-sentHello with alert-not-duplicated, from-idle, idempotency, post-armed no-op), BridgeNavigationFailureTests end-to-end + per-arm, MenuBarIconStateTransitionTests drives dormantAgentContinuation and asserts .thinking → .speaking → .idle. 62/62 Bus pass; all 18 boundary gates green.
Bus
Typed Swift↔JS message protocol over WKScriptMessageHandler. The webview is a pure rendering layer; this package is the only legitimate way Swift talks to the HUD and back.
Key public types
| Type | Purpose |
|---|---|
BusOutbound (enum) |
Messages Swift→JS — hello, hudState, tokenDelta, audioLevel, toolCallStart/End, turnStarted/Ended, sessionHistory |
BusInbound (enum) |
Messages JS→Swift — helloAck, uiReady, frameAttachRequested, chatSubmit, chatCancelAndSubmit |
OutboundBatcher (actor) |
Coalesces tokenDelta on a 32 ms tick; forwards everything else immediately |
WebviewBridge |
The WKScriptMessageHandler boundary (the only place that calls evaluateJavaScript) |
Handshake |
Version-negotiation flow on first uiReady |
BusReply / BusError |
Reply + error types for request/response patterns |
Depends on
Logging. External: swift-log.
Used by
App/AppDelegate, every subsystem that emits to the HUD, the webview-side TS package webview/packages/bus.
Key invariants / contracts
- Hand-written
Codable. BothBusOutboundandBusInbounddeclareCodablein extensions with explicitinit(from:)/encode(to:). Swift's synthesizedCodablewould produce{"hudState": {"_0": "idle"}}instead of{"type":"hudState","state":"idle"}. Adding a case without updating both methods is a compile error — schema-drift preventer at the language level. - Schema version sync.
Protocol.swiftcarries the version;webview/packages/bus/src/version.tscarries the TS mirror.scripts/check-bus-protocol-version.shrequires both to match. - Harness parity.
scripts/check-bus-harness-parity.shrequires the TS bus fixtures to round-trip through the Swift decoder. - No
evaluateJavaScriptoutsideWebviewBridge. Grep-gated byscripts/check-no-evaluate-javascript.sh. - Single writer to
HudState.scripts/check-single-writer-hudstate.shenforces.
Tests
58 XCTest. Round-trip encoding tests, version-negotiation tests, batcher coalescing tests, content-world isolation tests (WebviewBridgeOutboundTests asserts WKContentWorld.page).
Notable files
Sources/Bus/BusOutbound.swift— Swift→JS event union + hand-written CodableSources/Bus/BusInbound.swift— JS→Swift event union + hand-written CodableSources/Bus/OutboundBatcher.swift— token-coalescing actorSources/Bus/WebviewBridge.swift— WKScriptMessageHandler boundarySources/Bus/Protocol.swift— version constant