fix: render talk transcripts in native webchat

This commit is contained in:
Peter Steinberger
2026-05-02 02:46:59 +01:00
parent 225b71db1e
commit ff45bc1f88
8 changed files with 164 additions and 12 deletions

View File

@@ -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)"

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)])