Files
openclaw/apps/macos/Sources/OpenClaw/QuickChatReplyBinding.swift
Peter Steinberger 5143d909b9 feat(macos): Quick Chat streamed replies and continue-recent targeting (#110631)
* feat(macos): stream Quick Chat conversations

* docs(macos): explain Quick Chat recents

* fix(macos): bind the Quick Chat reply consumer before dispatching the send

* fix(macos): pre-bind reply consumer for all send paths; gate rebind on visible route

* fix(macos): revalidate recents eligibility after the async fetch

* fix(macos): invalidate in-flight recents fetches on competing interactions

* fix(macos): single-owner recents invalidation; fresh presentations clear prepared replies

* docs(macos): name the accepted scheduler-scale pre-bind tradeoff

* chore(i18n): refresh native locale artifacts for new Quick Chat strings

* fix(macos): keep Quick Chat placeholder and recents titles localizable
2026-07-18 15:58:49 +01:00

59 lines
2.3 KiB
Swift

import Observation
import OpenClawChatUI
@MainActor
@Observable
final class QuickChatReplyBinding {
typealias ViewModelFactory = @MainActor (QuickChatRoutingTarget) -> OpenClawChatViewModel
private(set) var route: QuickChatRoutingTarget?
private(set) var viewModel: OpenClawChatViewModel?
@ObservationIgnored private var preparedRoute: QuickChatRoutingTarget?
@ObservationIgnored private let viewModelFactory: ViewModelFactory
init(viewModelFactory: @escaping ViewModelFactory = QuickChatReplyBinding.makeViewModel) {
self.viewModelFactory = viewModelFactory
}
/// Starts the transport consumer before the send is dispatched so no early
/// delta/final frame is missed; the reply area stays hidden until show(route:).
/// Accepted tradeoff: construction does not synchronously install the transport
/// subscription (scheduler-scale gap). Deltas carry full snapshots, the shared
/// view model self-heals from them, and history bootstrap recovers committed
/// turns so only a turn completing within a runloop tick could be lost, which
/// is not a real production state and does not justify a readiness handshake in
/// the shared chat kit.
func prepare(route: QuickChatRoutingTarget) {
guard self.preparedRoute != route || self.viewModel == nil else { return }
self.preparedRoute = route
self.viewModel = self.viewModelFactory(route)
}
func show(route: QuickChatRoutingTarget) {
self.prepare(route: route)
self.route = route
}
func rebindIfActive(route: QuickChatRoutingTarget) {
// Only a VISIBLE reply rebinds; hidden prepared state from a failed send must
// not be promoted into an expanded transcript by a later target change.
guard self.route != nil else { return }
self.show(route: route)
}
func clear() {
self.route = nil
self.preparedRoute = nil
self.viewModel = nil
}
private static func makeViewModel(route: QuickChatRoutingTarget) -> OpenClawChatViewModel {
let transport = MacGatewayChatTransport(defaultGlobalAgentID: route.agentID)
return OpenClawChatViewModel(
sessionKey: route.sessionKey,
transport: transport,
activeAgentId: route.agentID)
}
}