mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-22 07:15:33 +00:00
fix(native): keep images and voice notes visible in chat and exports (#113764)
* fix(native): preserve image and audio in chat transcripts * fix(native): refresh extracted media localization source lines --------- Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
This commit is contained in:
parent
fe4972b12b
commit
a1d666fdc3
6 changed files with 154 additions and 36 deletions
|
|
@ -40003,7 +40003,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 635,
|
||||
"line": 628,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||
"source": "Voice note",
|
||||
"surface": "apple",
|
||||
|
|
@ -40011,7 +40011,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 692,
|
||||
"line": 685,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||
"source": "Writing",
|
||||
"surface": "apple",
|
||||
|
|
@ -40019,7 +40019,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 726,
|
||||
"line": 719,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||
"source": "Preparing audio…",
|
||||
"surface": "apple",
|
||||
|
|
@ -40027,7 +40027,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 729,
|
||||
"line": 722,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||
"source": "Speaking…",
|
||||
"surface": "apple",
|
||||
|
|
@ -40035,7 +40035,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 737,
|
||||
"line": 730,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||
"source": "Preparing audio, tap to cancel",
|
||||
"surface": "apple",
|
||||
|
|
@ -40043,7 +40043,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 738,
|
||||
"line": 731,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||
"source": "Speaking, tap to stop",
|
||||
"surface": "apple",
|
||||
|
|
@ -41219,7 +41219,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 125,
|
||||
"line": 122,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTranscriptExporter.swift",
|
||||
"source": "Message",
|
||||
"surface": "apple",
|
||||
|
|
@ -41275,7 +41275,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 1145,
|
||||
"line": 1138,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Copy Message",
|
||||
"surface": "apple",
|
||||
|
|
@ -41283,7 +41283,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 1168,
|
||||
"line": 1161,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Open Full Message",
|
||||
"surface": "apple",
|
||||
|
|
@ -41291,7 +41291,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 1187,
|
||||
"line": 1180,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Rewind to Here",
|
||||
"surface": "apple",
|
||||
|
|
@ -41299,7 +41299,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 1207,
|
||||
"line": 1200,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Fork from Here",
|
||||
"surface": "apple",
|
||||
|
|
@ -41307,7 +41307,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1233,
|
||||
"line": 1226,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Reply",
|
||||
"surface": "apple",
|
||||
|
|
@ -41315,7 +41315,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1243,
|
||||
"line": 1236,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "You",
|
||||
"surface": "apple",
|
||||
|
|
@ -41323,7 +41323,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1245,
|
||||
"line": 1238,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Assistant",
|
||||
"surface": "apple",
|
||||
|
|
@ -41331,7 +41331,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 1311,
|
||||
"line": 1304,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Loading chat",
|
||||
"surface": "apple",
|
||||
|
|
@ -41339,7 +41339,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 1391,
|
||||
"line": 1384,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||
"source": "Dismiss",
|
||||
"surface": "apple",
|
||||
|
|
|
|||
|
|
@ -501,14 +501,7 @@ private struct ChatMessageBody: View {
|
|||
}
|
||||
|
||||
private var inlineAttachments: [OpenClawChatMessageContent] {
|
||||
self.message.content.filter { content in
|
||||
switch content.type ?? "text" {
|
||||
case "file", "attachment":
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
self.message.content.filter(\.isInlineAttachment)
|
||||
}
|
||||
|
||||
private var inlineWidgets: [OpenClawChatCanvasPreview] {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,16 @@ public struct OpenClawChatMessageContent: Codable, Hashable, Sendable {
|
|||
public let details: AnyCodable?
|
||||
public let isError: Bool?
|
||||
|
||||
/// Gateway media and historical file attachments must stay visible in both chat and exports.
|
||||
var isInlineAttachment: Bool {
|
||||
switch self.type?.lowercased() {
|
||||
case "file", "attachment", "image", "audio":
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
type: String?,
|
||||
text: String?,
|
||||
|
|
|
|||
|
|
@ -109,10 +109,7 @@ public enum ChatTranscriptExporter {
|
|||
}
|
||||
|
||||
private static func attachments(in message: OpenClawChatMessage) -> [OpenClawChatMessageContent] {
|
||||
message.content.filter { content in
|
||||
let kind = (content.type ?? "text").lowercased()
|
||||
return kind == "file" || kind == "attachment"
|
||||
}
|
||||
message.content.filter(\.isInlineAttachment)
|
||||
}
|
||||
|
||||
private static func displayRole(_ role: String) -> String {
|
||||
|
|
|
|||
|
|
@ -1090,14 +1090,7 @@ extension OpenClawChatView {
|
|||
}
|
||||
|
||||
private func hasInlineAttachments(in message: OpenClawChatMessage) -> Bool {
|
||||
message.content.contains { content in
|
||||
switch content.type ?? "text" {
|
||||
case "file", "attachment":
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
message.content.contains(where: \.isInlineAttachment)
|
||||
}
|
||||
|
||||
private func toolCalls(in message: OpenClawChatMessage) -> [OpenClawChatMessageContent] {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,131 @@ struct ChatTranscriptExporterTests {
|
|||
""")
|
||||
}
|
||||
|
||||
@Test(arguments: ["file", "attachment", "image", "audio", "FILE", "ATTACHMENT", "IMAGE", "AuDiO"])
|
||||
func `classifies gateway media and historical file attachments consistently`(type: String) {
|
||||
let content = OpenClawChatMessageContent(
|
||||
type: type,
|
||||
text: nil,
|
||||
mimeType: nil,
|
||||
fileName: nil,
|
||||
content: nil)
|
||||
|
||||
#expect(content.isInlineAttachment)
|
||||
}
|
||||
|
||||
@Test(arguments: ["text", "canvas", "toolCall", "tool_result"])
|
||||
func `does not classify unrelated content as an attachment`(type: String) {
|
||||
let content = OpenClawChatMessageContent(
|
||||
type: type,
|
||||
text: nil,
|
||||
mimeType: "image/png",
|
||||
fileName: "not-an-attachment.png",
|
||||
content: nil)
|
||||
|
||||
#expect(!content.isInlineAttachment)
|
||||
}
|
||||
|
||||
@Test func `does not classify content without a type as an attachment`() {
|
||||
let content = OpenClawChatMessageContent(
|
||||
type: nil,
|
||||
text: nil,
|
||||
mimeType: "image/png",
|
||||
fileName: "not-an-attachment.png",
|
||||
content: nil)
|
||||
|
||||
#expect(!content.isInlineAttachment)
|
||||
}
|
||||
|
||||
@Test(arguments: ["user", "assistant"], ["image", "audio", "IMAGE", "AuDiO"])
|
||||
func `exports canonical media-only messages`(role: String, type: String) {
|
||||
let isAudio = type.lowercased() == "audio"
|
||||
let filename = isAudio ? "voice-note.m4a" : "photo.png"
|
||||
let message = OpenClawChatMessage(
|
||||
role: role,
|
||||
content: [
|
||||
OpenClawChatMessageContent(
|
||||
type: type,
|
||||
text: nil,
|
||||
mimeType: isAudio ? "audio/mp4" : "image/png",
|
||||
fileName: filename,
|
||||
content: nil),
|
||||
],
|
||||
timestamp: 0)
|
||||
|
||||
let markdown = ChatTranscriptExporter.markdown(
|
||||
sessionTitle: "Media chat",
|
||||
sessionKey: "agent:main",
|
||||
messages: [message])
|
||||
|
||||
#expect(markdown == """
|
||||
# Media chat
|
||||
|
||||
### \(role == "user" ? "User" : "Assistant") — 1970-01-01T00:00:00Z
|
||||
|
||||
_[attachment: \(filename)]_
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
@Test(arguments: ["image", "audio", "IMAGE", "AuDiO"])
|
||||
func `preserves canonical media alongside message text`(type: String) {
|
||||
let isAudio = type.lowercased() == "audio"
|
||||
let filename = isAudio ? "voice-note.m4a" : "photo.png"
|
||||
let message = OpenClawChatMessage(
|
||||
role: "user",
|
||||
content: [
|
||||
OpenClawChatMessageContent(
|
||||
type: "text",
|
||||
text: "Here is the attachment.",
|
||||
mimeType: nil,
|
||||
fileName: nil,
|
||||
content: nil),
|
||||
OpenClawChatMessageContent(
|
||||
type: type,
|
||||
text: nil,
|
||||
mimeType: isAudio ? "audio/mp4" : "image/png",
|
||||
fileName: filename,
|
||||
content: nil),
|
||||
],
|
||||
timestamp: 0)
|
||||
|
||||
let markdown = ChatTranscriptExporter.markdown(
|
||||
sessionTitle: "Media chat",
|
||||
sessionKey: "agent:main",
|
||||
messages: [message])
|
||||
|
||||
#expect(markdown == """
|
||||
# Media chat
|
||||
|
||||
### User — 1970-01-01T00:00:00Z
|
||||
|
||||
Here is the attachment.
|
||||
|
||||
_[attachment: \(filename)]_
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
@Test(arguments: ["canvas", "toolCall", "tool_result"])
|
||||
func `does not export non-attachment content as media`(type: String) {
|
||||
let message = OpenClawChatMessage(
|
||||
role: "assistant",
|
||||
content: [
|
||||
OpenClawChatMessageContent(
|
||||
type: type,
|
||||
text: nil,
|
||||
mimeType: "image/png",
|
||||
fileName: "not-an-attachment.png",
|
||||
content: nil),
|
||||
],
|
||||
timestamp: 0)
|
||||
|
||||
#expect(ChatTranscriptExporter.markdown(
|
||||
sessionTitle: "Media chat",
|
||||
sessionKey: "agent:main",
|
||||
messages: [message]) == "# Media chat\n")
|
||||
}
|
||||
|
||||
@Test func `sanitizes filename`() {
|
||||
#expect(
|
||||
ChatTranscriptExporter.filename(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue