mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:30:42 +00:00
fix: render talk transcripts in native webchat
This commit is contained in:
@@ -269,6 +269,25 @@ public struct OpenClawChatEventPayload: Codable, Sendable {
|
||||
public let errorMessage: String?
|
||||
}
|
||||
|
||||
public struct OpenClawSessionMessageEventPayload: Codable, Sendable {
|
||||
public let sessionKey: String?
|
||||
public let message: OpenClawChatMessage?
|
||||
public let messageId: String?
|
||||
public let messageSeq: Int?
|
||||
|
||||
public init(
|
||||
sessionKey: String?,
|
||||
message: OpenClawChatMessage?,
|
||||
messageId: String?,
|
||||
messageSeq: Int?)
|
||||
{
|
||||
self.sessionKey = sessionKey
|
||||
self.message = message
|
||||
self.messageId = messageId
|
||||
self.messageSeq = messageSeq
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawAgentEventPayload: Codable, Sendable, Identifiable {
|
||||
public var id: String {
|
||||
"\(self.runId)-\(self.seq ?? -1)"
|
||||
|
||||
@@ -4,6 +4,7 @@ public enum OpenClawChatTransportEvent: Sendable {
|
||||
case health(ok: Bool)
|
||||
case tick
|
||||
case chat(OpenClawChatEventPayload)
|
||||
case sessionMessage(OpenClawSessionMessageEventPayload)
|
||||
case agent(OpenClawAgentEventPayload)
|
||||
case seqGap
|
||||
}
|
||||
|
||||
@@ -950,6 +950,8 @@ public final class OpenClawChatViewModel {
|
||||
Task { await self.pollHealthIfNeeded(force: false) }
|
||||
case let .chat(chat):
|
||||
self.handleChatEvent(chat)
|
||||
case let .sessionMessage(message):
|
||||
self.handleSessionMessageEvent(message)
|
||||
case let .agent(agent):
|
||||
self.handleAgentEvent(agent)
|
||||
case .seqGap:
|
||||
@@ -962,6 +964,26 @@ public final class OpenClawChatViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSessionMessageEvent(_ payload: OpenClawSessionMessageEventPayload) {
|
||||
if let sessionKey = payload.sessionKey,
|
||||
!Self.matchesCurrentSessionKey(incoming: sessionKey, current: self.sessionKey)
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
guard let message = payload.message else { return }
|
||||
guard message.role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "user" else {
|
||||
return
|
||||
}
|
||||
if self.pendingRunCount > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
let sanitized = Self.stripInboundMetadata(from: message)
|
||||
let reconciled = Self.reconcileMessageIDs(previous: self.messages, incoming: self.messages + [sanitized])
|
||||
self.messages = Self.dedupeMessages(reconciled)
|
||||
}
|
||||
|
||||
private func handleChatEvent(_ chat: OpenClawChatEventPayload) {
|
||||
let isOurRun = chat.runId.flatMap { self.pendingRuns.contains($0) } ?? false
|
||||
|
||||
|
||||
@@ -689,6 +689,69 @@ extension TestChatTransportState {
|
||||
}
|
||||
}
|
||||
|
||||
@Test func appendsExternalSessionUserMessageForActiveSession() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let (transport, vm) = await makeViewModel(historyResponses: [historyPayload()])
|
||||
|
||||
await MainActor.run { vm.load() }
|
||||
try await waitUntil("bootstrap history loaded") { await MainActor.run { vm.messages.isEmpty } }
|
||||
|
||||
transport.emit(
|
||||
.sessionMessage(
|
||||
OpenClawSessionMessageEventPayload(
|
||||
sessionKey: "agent:main:main",
|
||||
message: OpenClawChatMessage(
|
||||
role: "user",
|
||||
content: [
|
||||
OpenClawChatMessageContent(
|
||||
type: "text",
|
||||
text: "spoken transcript",
|
||||
mimeType: nil,
|
||||
fileName: nil,
|
||||
content: nil),
|
||||
],
|
||||
timestamp: now),
|
||||
messageId: "msg-1",
|
||||
messageSeq: 1)))
|
||||
|
||||
try await waitUntil("external transcript visible") {
|
||||
await MainActor.run {
|
||||
vm.messages.count == 1 &&
|
||||
vm.messages.first?.role == "user" &&
|
||||
vm.messages.first?.content.first?.text == "spoken transcript"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func ignoresExternalSessionUserMessageForOtherSession() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let (transport, vm) = await makeViewModel(historyResponses: [historyPayload()])
|
||||
|
||||
await MainActor.run { vm.load() }
|
||||
try await waitUntil("bootstrap history loaded") { await MainActor.run { vm.messages.isEmpty } }
|
||||
|
||||
transport.emit(
|
||||
.sessionMessage(
|
||||
OpenClawSessionMessageEventPayload(
|
||||
sessionKey: "other",
|
||||
message: OpenClawChatMessage(
|
||||
role: "user",
|
||||
content: [
|
||||
OpenClawChatMessageContent(
|
||||
type: "text",
|
||||
text: "other transcript",
|
||||
mimeType: nil,
|
||||
fileName: nil,
|
||||
content: nil),
|
||||
],
|
||||
timestamp: now),
|
||||
messageId: "msg-2",
|
||||
messageSeq: 2)))
|
||||
|
||||
try await Task.sleep(nanoseconds: 50_000_000)
|
||||
#expect(await MainActor.run { vm.messages.isEmpty })
|
||||
}
|
||||
|
||||
@Test func preservesMessageIDsAcrossHistoryRefreshes() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let history1 = historyPayload(messages: [chatTextMessage(role: "user", text: "hello", timestamp: now)])
|
||||
|
||||
Reference in New Issue
Block a user