File size: 2,973 Bytes
b8370f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | import Foundation
/// Plain-text projection of a transcript message: exactly what the reader sees
/// in the bubble, with tool traces and non-text blocks removed. Shared by the
/// transcript exporter and the Listen action so exported and spoken text
/// always match the visible transcript.
public enum ChatMessageVisibleText {
static func isVisibleContentType(_ type: String?, role: String) -> Bool {
let kind = type?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? ""
if kind.isEmpty || kind == "text" {
return true
}
let normalizedRole = role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if kind == "input_text" {
return normalizedRole == "user" || normalizedRole == "assistant"
}
return normalizedRole == "assistant" && kind == "output_text"
}
static func copyText(in message: OpenClawChatMessage) -> String {
let role = message.role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return role == "assistant" ? self.visibleText(in: message) : self.primaryText(in: message)
}
public static func visibleText(in message: OpenClawChatMessage) -> String {
let text = self.primaryText(in: message)
let role = message.role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard role != "user" else { return text }
return AssistantTextParser.visibleSegments(from: text)
.map(\.text)
.joined(separator: "\n\n")
}
static func hasVisibleText(in message: OpenClawChatMessage) -> Bool {
!self.visibleText(in: message)
.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty
}
static func displayText(in message: OpenClawChatMessage, includeThinking: Bool) -> String {
let isAssistant = message.role.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased() == "assistant"
let parts = message.content.compactMap { content -> String? in
let kind = content.type?.lowercased() ?? ""
if self.isVisibleContentType(kind, role: message.role) {
return content.text
}
guard
includeThinking,
isAssistant,
kind == "thinking",
let thinking = content.thinking,
!thinking.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
else {
return nil
}
return "<think>\n\(thinking)\n</think>"
}
return OpenClawChatMessage.displayText(
contentText: parts.joined(separator: "\n"),
role: message.role,
stopReason: message.stopReason,
errorMessage: message.errorMessage)
}
private static func primaryText(in message: OpenClawChatMessage) -> String {
self.displayText(in: message, includeThinking: false)
}
}
|