@@ -2,6 +2,7 @@ import Foundation
22import SwiftUI
33
44@Observable
5+@MainActor
56final class QueryViewModel {
67 var sqlQuery = "" {
78 didSet {
@@ -24,14 +25,20 @@ final class QueryViewModel {
2425 // re-applies the filter.
2526 updateFilteredResults()
2627 let snapshot = results
27− searchTextTask = Task {
28− let texts = snapshot.map { record in
29− record.values.map { $0.displayString }.joined(separator: " ")
30− }
31− guard !Task.isCancelled else { return }
32− cachedSearchTexts = texts
33− if !filterText.isEmpty {
34− updateFilteredResults()
28+ // The class is @MainActor, so this Task inherits main-actor isolation —
29+ // its mutations of cachedSearchTexts/filteredResults are safely serialized
30+ // with SwiftUI reads. Only the pure (and potentially large) displayString
31+ // mapping is pushed off-main via Task.detached so it doesn't block the UI.
32+ searchTextTask = Task { [weak self] in
33+ let texts = await Task.detached(priority: .userInitiated) {
34+ snapshot.map { record in
35+ record.values.map { $0.displayString }.joined(separator: " ")
36+ }
37+ }.value
38+ guard let self, !Task.isCancelled else { return }
39+ self.cachedSearchTexts = texts
40+ if !self.filterText.isEmpty {
41+ self.updateFilteredResults()
3542 }
3643 }
3744 }
@@ -551,7 +558,7 @@ final class QueryViewModel {
551558 /// Projects each record down to the given columns so JSON export, like CSV,
552559 /// respects hidden columns instead of leaking every field. Safe to call from
553560 /// any thread.
554− static func projectRecords(_ records: [LogRecord], to columns: [String]) -> [LogRecord] {
561+ nonisolated static func projectRecords(_ records: [LogRecord], to columns: [String]) -> [LogRecord] {
555562 // No column list means no restriction (columns not computed yet) — export
556563 // the full records rather than stripping every field to an empty object.
557564 guard !columns.isEmpty else { return records }
@@ -560,7 +567,7 @@ final class QueryViewModel {
560567 }
561568
562569 /// Builds a CSV string from records and columns. Safe to call from any thread.
563− static func buildCSV(records: [LogRecord], columns: [String]) -> String {
570+ nonisolated static func buildCSV(records: [LogRecord], columns: [String]) -> String {
564571 guard !records.isEmpty, !columns.isEmpty else { return "" }
565572
566573 var csv = String()
@@ -586,7 +593,7 @@ final class QueryViewModel {
586593 return csv
587594 }
588595
589− private static func escapeCSV(_ value: String) -> String {
596+ nonisolated private static func escapeCSV(_ value: String) -> String {
590597 // Check at the Unicode scalar level to correctly detect \r and \n
591598 // inside \r\n (CRLF) grapheme clusters, which Swift's String.contains
592599 // treats as a single Character that doesn't match "\r" or "\n" alone.
@@ -731,7 +738,7 @@ final class QueryViewModel {
731738 }
732739
733740 /// Escapes a SQL identifier by doubling internal double-quotes, then wrapping in double-quotes.
734− static func escapeSQLIdentifier(_ identifier: String) -> String {
741+ nonisolated static func escapeSQLIdentifier(_ identifier: String) -> String {
735742 "\"\(identifier.replacingOccurrences(of: "\"", with: "\"\""))\""
736743 }
737744