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