mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 21:11:14 +00:00
* feat(chat): replay the active plan snapshot to reconnecting clients Plan checklists were live-event-only: a client that connected or refreshed mid-run saw nothing until the next update_plan call, and iOS adoptRun actively wiped retained plan state on reconnect. The gateway now records the latest normalized plan snapshot per run beside the existing chat text buffers and replays it through the established chat.history/chat.startup inFlightRun recovery contract (runId > plan > text under the payload budget; a budget-dropped plan is sent as an explicit empty snapshot so clients never preserve a stale checklist). Web, iOS, and Android seed and reconcile the snapshot through one shared contract riding each client's existing inFlightRun adoption gate: present plan replaces, absent plan preserves (legacy gateway), a different or terminated run clears, and stale history responses cannot clobber a newer live plan. Verified live gap: openclaw/openclaw#108675 comment 5001475066. * fix(ios): extract run-snapshot adoption below the file-length lint ceiling ChatViewModel.swift crossed SwiftLint's 1500-line limit; the in-flight snapshot adoption and plan reconciliation move to a focused extension. Also refresh the native i18n inventory for shifted line anchors. * fixup: regenerate i18n anchors and test formatting after rebase * ci: retrigger after wedged queued runs * fix(ui): use Object.hasOwn for the plan field presence probe * fix(ios): annotate the plan-snapshot fixture init for periphery
84 lines
2.9 KiB
Swift
84 lines
2.9 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
|
|
/// Live plan-checklist state: full-snapshot semantics scoped to the owning run.
|
|
extension OpenClawChatViewModel {
|
|
func applyPlanSnapshot(runId: String, data: [String: AnyCodable]) {
|
|
let steps = OpenClawChatPlanStep.parseSteps(data["steps"])
|
|
let explanation = data["explanation"]?.value as? String
|
|
self.applyPlanSnapshot(runId: runId, steps: steps, explanation: explanation)
|
|
}
|
|
|
|
func applyPlanSnapshot(
|
|
runId: String,
|
|
steps: [OpenClawChatPlanStep],
|
|
explanation: String?)
|
|
{
|
|
guard !steps.isEmpty else {
|
|
self.clearPlan(for: runId)
|
|
return
|
|
}
|
|
let trimmedExplanation = explanation?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let normalizedExplanation = trimmedExplanation?.isEmpty == false ? trimmedExplanation : nil
|
|
guard planRunId != runId ||
|
|
planSteps != steps ||
|
|
planExplanation != normalizedExplanation
|
|
else {
|
|
return
|
|
}
|
|
planRunId = runId
|
|
planSteps = steps
|
|
planExplanation = normalizedExplanation
|
|
markTimelineChanged()
|
|
}
|
|
|
|
func clearPlan(for runId: String? = nil) {
|
|
if let runId, planRunId != runId {
|
|
return
|
|
}
|
|
guard planRunId != nil || !planSteps.isEmpty || planExplanation != nil else { return }
|
|
planRunId = nil
|
|
planSteps = []
|
|
planExplanation = nil
|
|
markTimelineChanged()
|
|
}
|
|
}
|
|
|
|
/// Session-run activity indicator: shares the run-progress surface with the
|
|
/// plan checklist (both gate on an active run without a chat snapshot).
|
|
extension OpenClawChatViewModel {
|
|
func updateActiveSessionRunWithoutChatSnapshot(_ active: Bool) {
|
|
guard self.hasActiveSessionRunWithoutChatSnapshot != active else { return }
|
|
self.hasActiveSessionRunWithoutChatSnapshot = active
|
|
if active {
|
|
self.armActiveSessionRunIndicatorTimeout()
|
|
} else {
|
|
self.activeSessionRunIndicatorTimeoutTask?.cancel()
|
|
self.activeSessionRunIndicatorTimeoutTask = nil
|
|
}
|
|
self.markTimelineChanged()
|
|
}
|
|
|
|
private func armActiveSessionRunIndicatorTimeout() {
|
|
self.activeSessionRunIndicatorTimeoutTask?.cancel()
|
|
let timeoutMs = self.pendingRunWaitTimeoutMs
|
|
self.activeSessionRunIndicatorTimeoutTask = Task { [weak self] in
|
|
do {
|
|
try await Task.sleep(nanoseconds: timeoutMs * 1_000_000)
|
|
} catch {
|
|
return
|
|
}
|
|
await MainActor.run {
|
|
self?.updateActiveSessionRunWithoutChatSnapshot(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func clearActiveSessionRunIndicatorIfLatestUserAnswered() {
|
|
guard self.hasActiveSessionRunWithoutChatSnapshot,
|
|
!Self.hasUnansweredLatestUser(in: self.messages)
|
|
else { return }
|
|
self.updateActiveSessionRunWithoutChatSnapshot(false)
|
|
}
|
|
}
|