ui: merge tool call results
This commit is contained in:
@@ -213,6 +213,16 @@ private struct ChatMessageBody: View {
|
|||||||
isUser: self.isUser)
|
isUser: self.isUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.inlineToolResults.isEmpty {
|
||||||
|
ForEach(self.inlineToolResults.indices, id: \.self) { idx in
|
||||||
|
let toolResult = self.inlineToolResults[idx]
|
||||||
|
ToolResultCard(
|
||||||
|
title: toolResult.name ?? "Tool result",
|
||||||
|
text: toolResult.text ?? "",
|
||||||
|
isUser: self.isUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
@@ -227,7 +237,11 @@ private struct ChatMessageBody: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var primaryText: String {
|
private var primaryText: String {
|
||||||
let parts = self.message.content.compactMap(\.text)
|
let parts = self.message.content.compactMap { content -> String? in
|
||||||
|
let kind = (content.type ?? "text").lowercased()
|
||||||
|
guard kind == "text" || kind.isEmpty else { return nil }
|
||||||
|
return content.text
|
||||||
|
}
|
||||||
return parts.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
|
return parts.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +266,13 @@ private struct ChatMessageBody: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var inlineToolResults: [ClawdisChatMessageContent] {
|
||||||
|
self.message.content.filter { content in
|
||||||
|
let kind = (content.type ?? "").lowercased()
|
||||||
|
return kind == "toolresult" || kind == "tool_result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var isToolResultMessage: Bool {
|
private var isToolResultMessage: Bool {
|
||||||
let role = self.message.role.lowercased()
|
let role = self.message.role.lowercased()
|
||||||
return role == "toolresult" || role == "tool_result"
|
return role == "toolresult" || role == "tool_result"
|
||||||
|
|||||||
@@ -133,9 +133,96 @@ public struct ClawdisChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var visibleMessages: [ClawdisChatMessage] {
|
private var visibleMessages: [ClawdisChatMessage] {
|
||||||
guard self.style == .onboarding else { return self.viewModel.messages }
|
let base: [ClawdisChatMessage]
|
||||||
guard let first = self.viewModel.messages.first else { return [] }
|
if self.style == .onboarding {
|
||||||
guard first.role.lowercased() == "user" else { return self.viewModel.messages }
|
guard let first = self.viewModel.messages.first else { return [] }
|
||||||
return Array(self.viewModel.messages.dropFirst())
|
base = first.role.lowercased() == "user" ? Array(self.viewModel.messages.dropFirst()) : self.viewModel.messages
|
||||||
|
} else {
|
||||||
|
base = self.viewModel.messages
|
||||||
|
}
|
||||||
|
return self.mergeToolResults(in: base)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergeToolResults(in messages: [ClawdisChatMessage]) -> [ClawdisChatMessage] {
|
||||||
|
var result: [ClawdisChatMessage] = []
|
||||||
|
result.reserveCapacity(messages.count)
|
||||||
|
|
||||||
|
for message in messages {
|
||||||
|
guard self.isToolResultMessage(message) else {
|
||||||
|
result.append(message)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let toolCallId = message.toolCallId,
|
||||||
|
let last = result.last,
|
||||||
|
self.toolCallIds(in: last).contains(toolCallId)
|
||||||
|
else {
|
||||||
|
result.append(message)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let toolText = self.toolResultText(from: message)
|
||||||
|
if toolText.isEmpty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = last.content
|
||||||
|
content.append(
|
||||||
|
ClawdisChatMessageContent(
|
||||||
|
type: "tool_result",
|
||||||
|
text: toolText,
|
||||||
|
thinking: nil,
|
||||||
|
thinkingSignature: nil,
|
||||||
|
mimeType: nil,
|
||||||
|
fileName: nil,
|
||||||
|
content: nil,
|
||||||
|
id: toolCallId,
|
||||||
|
name: message.toolName,
|
||||||
|
arguments: nil))
|
||||||
|
|
||||||
|
let merged = ClawdisChatMessage(
|
||||||
|
id: last.id,
|
||||||
|
role: last.role,
|
||||||
|
content: content,
|
||||||
|
timestamp: last.timestamp,
|
||||||
|
toolCallId: last.toolCallId,
|
||||||
|
toolName: last.toolName,
|
||||||
|
usage: last.usage,
|
||||||
|
stopReason: last.stopReason)
|
||||||
|
result[result.count - 1] = merged
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isToolResultMessage(_ message: ClawdisChatMessage) -> Bool {
|
||||||
|
let role = message.role.lowercased()
|
||||||
|
return role == "toolresult" || role == "tool_result"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toolCallIds(in message: ClawdisChatMessage) -> Set<String> {
|
||||||
|
var ids = Set<String>()
|
||||||
|
for content in message.content {
|
||||||
|
let kind = (content.type ?? "").lowercased()
|
||||||
|
let isTool =
|
||||||
|
["toolcall", "tool_call", "tooluse", "tool_use"].contains(kind) ||
|
||||||
|
(content.name != nil && content.arguments != nil)
|
||||||
|
if isTool, let id = content.id {
|
||||||
|
ids.insert(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let toolCallId = message.toolCallId {
|
||||||
|
ids.insert(toolCallId)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toolResultText(from message: ClawdisChatMessage) -> String {
|
||||||
|
let parts = message.content.compactMap { content -> String? in
|
||||||
|
let kind = (content.type ?? "text").lowercased()
|
||||||
|
guard kind == "text" || kind.isEmpty else { return nil }
|
||||||
|
return content.text
|
||||||
|
}
|
||||||
|
return parts.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user