#if os(macOS) import SwiftUI struct ChatTranscriptSearchResults: Equatable { let messageIDs: [UUID] init(rows: [ChatTranscriptRow], query: String) { let query = query.trimmingCharacters(in: .whitespacesAndNewlines) self.messageIDs = query.isEmpty ? [] : rows.compactMap { row in guard case let .message(message) = row, ["user", "assistant"].contains(message.role.lowercased()), ChatMessageVisibleText.visibleText(in: message).localizedStandardContains(query) else { return nil } return message.id } } func selection(after current: UUID?, backwards: Bool = false) -> UUID? { guard !self.messageIDs.isEmpty else { return nil } guard let current, let index = self.messageIDs.firstIndex(of: current) else { return backwards ? self.messageIDs.last : self.messageIDs.first } let offset = backwards ? self.messageIDs.count - 1 : 1 return self.messageIDs[(index + offset) % self.messageIDs.count] } } struct ChatTranscriptSearch: ViewModifier { let rows: [ChatTranscriptRow] let sessionKey: String let isEnabled: Bool @Binding var selectedMessageID: UUID? @Binding var isPresented: Bool let onSelect: (UUID) -> Void @State private var query = "" @FocusState private var isFocused: Bool func body(content: Content) -> some View { let results = ChatTranscriptSearchResults(rows: self.rows, query: self.query) return content .safeAreaInset(edge: .top, spacing: 0) { if self.isPresented { self.searchBar(results: results) } } .toolbar { if self.isEnabled { ToolbarItem(placement: .primaryAction) { Button { self.isPresented = true self.isFocused = true } label: { Label("Find in Conversation", systemImage: "magnifyingglass") } .keyboardShortcut("f", modifiers: .command) .help("Find in conversation (⌘F)") } } } .onChange(of: results) { _, results in guard self.isPresented else { return } if !results.messageIDs.contains(where: { $0 == self.selectedMessageID }) { self.select(results.messageIDs.first) } } .onChange(of: self.sessionKey) { _, _ in self.close() } .onChange(of: self.isPresented) { _, isPresented in if !isPresented { self.close() } } } private func searchBar(results: ChatTranscriptSearchResults) -> some View { HStack(spacing: 10) { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) TextField("Find in loaded messages", text: self.$query) .textFieldStyle(.plain) .focused(self.$isFocused) .onAppear { self.isFocused = true } .onSubmit { self.selectNext(in: results) } .onExitCommand { self.close() } .accessibilityIdentifier("chat-transcript-search") Text(self.resultLabel(results: results)) .font(OpenClawChatTypography.caption) .foregroundStyle(.secondary) .monospacedDigit() .fixedSize() Button { self.selectNext(in: results, backwards: true) } label: { Label("Previous Match", systemImage: "chevron.up") } .keyboardShortcut("g", modifiers: [.command, .shift]) .disabled(results.messageIDs.isEmpty) .help("Previous match (⇧⌘G)") Button { self.selectNext(in: results) } label: { Label("Next Match", systemImage: "chevron.down") } .keyboardShortcut("g", modifiers: .command) .disabled(results.messageIDs.isEmpty) .help("Next match (⌘G)") Button(action: self.close) { Label("Close Find", systemImage: "xmark") } .help("Close find (Escape)") } .labelStyle(.iconOnly) .buttonStyle(.borderless) .padding(.horizontal, 16) .padding(.vertical, 12) .background(.bar) .overlay(alignment: .bottom) { Divider() } } private func resultLabel(results: ChatTranscriptSearchResults) -> String { if self.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return "" } guard let selectedMessageID, let index = results.messageIDs.firstIndex(of: selectedMessageID) else { return String(localized: "No matches") } return String( format: String(localized: "%lld of %lld"), Int64(index + 1), Int64(results.messageIDs.count)) } private func selectNext(in results: ChatTranscriptSearchResults, backwards: Bool = false) { self.select(results.selection(after: self.selectedMessageID, backwards: backwards)) } private func select(_ messageID: UUID?) { self.selectedMessageID = messageID if let messageID { self.onSelect(messageID) } } private func close() { self.isPresented = false self.query = "" self.selectedMessageID = nil } } #endif