iCloud Backup, better chatview exp. bugfixes++

This commit is contained in:
2026-02-27 14:05:11 +01:00
parent 3997f3feee
commit e9d0ad3c66
11 changed files with 634 additions and 30 deletions

View File

@@ -33,6 +33,8 @@ struct MessageRow: View {
let viewModel: ChatViewModel?
private let settings = SettingsService.shared
@State private var isExpanded = false
#if os(macOS)
@State private var isHovering = false
@State private var showCopied = false
@@ -82,8 +84,8 @@ struct MessageRow: View {
.help("Star this message to always include it in context")
}
// Copy button (assistant messages only, visible on hover)
if message.role == .assistant && isHovering && !message.content.isEmpty {
// Copy button (user + assistant messages, visible on hover)
if (message.role == .assistant || message.role == .user) && isHovering && !message.content.isEmpty {
Button(action: copyContent) {
HStack(spacing: 3) {
Image(systemName: showCopied ? "checkmark" : "doc.on.doc")
@@ -188,27 +190,126 @@ struct MessageRow: View {
@ViewBuilder
private var compactSystemMessage: some View {
HStack(spacing: 8) {
Image(systemName: "wrench.and.screwdriver")
.font(.system(size: 11))
.foregroundColor(.secondary)
let expandable = message.toolCalls != nil
VStack(alignment: .leading, spacing: 0) {
Button(action: {
if expandable {
withAnimation(.easeInOut(duration: 0.18)) { isExpanded.toggle() }
}
}) {
HStack(spacing: 8) {
Image(systemName: "wrench.and.screwdriver")
.font(.system(size: 11))
.foregroundColor(.secondary)
Text(message.content)
.font(.system(size: 11))
.foregroundColor(.secondary)
Text(message.content)
.font(.system(size: 11))
.foregroundColor(.secondary)
Spacer()
Spacer()
Text(message.timestamp, style: .time)
.font(.system(size: 10))
.foregroundColor(.secondary.opacity(0.7))
if expandable {
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
.font(.system(size: 9, weight: .medium))
.foregroundColor(.secondary.opacity(0.5))
}
Text(message.timestamp, style: .time)
.font(.system(size: 10))
.foregroundColor(.secondary.opacity(0.7))
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if isExpanded, let calls = message.toolCalls {
Divider()
.padding(.horizontal, 8)
toolCallsDetailView(calls)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.secondary.opacity(0.08))
.cornerRadius(6)
}
@ViewBuilder
private func toolCallsDetailView(_ calls: [ToolCallDetail]) -> some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(calls.indices, id: \.self) { i in
let call = calls[i]
VStack(alignment: .leading, spacing: 6) {
// Tool name + status
HStack(spacing: 6) {
Image(systemName: "function")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.secondary)
Text(call.name)
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
Spacer()
if call.result == nil {
ProgressView()
.scaleEffect(0.5)
.frame(width: 12, height: 12)
} else {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 10))
.foregroundColor(.green.opacity(0.8))
}
}
// Input
if !call.input.isEmpty && call.input != "{}" {
toolDetailSection(label: "Input", text: prettyJSON(call.input), maxHeight: 100)
}
// Result
if let result = call.result {
toolDetailSection(label: "Result", text: prettyJSON(result), maxHeight: 180)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
if i < calls.count - 1 {
Divider().padding(.horizontal, 12)
}
}
}
}
@ViewBuilder
private func toolDetailSection(label: String, text: String, maxHeight: CGFloat) -> some View {
VStack(alignment: .leading, spacing: 2) {
Text(label.uppercased())
.font(.system(size: 9, weight: .semibold))
.foregroundColor(.secondary.opacity(0.6))
ScrollView([.vertical, .horizontal]) {
Text(text)
.font(.system(size: 10, design: .monospaced))
.foregroundColor(.secondary)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxHeight: maxHeight)
.background(Color.secondary.opacity(0.06))
.cornerRadius(4)
}
}
private func prettyJSON(_ raw: String) -> String {
guard let data = raw.data(using: .utf8),
let obj = try? JSONSerialization.jsonObject(with: data),
let pretty = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted, .sortedKeys]),
let str = String(data: pretty, encoding: .utf8) else {
return raw
}
return str
}
// MARK: - Message Content
@ViewBuilder