UI redesign Phase 1: NavigationSplitView with collapsible sidebar
- Replace root VStack with NavigationSplitView (2-column, collapsible sidebar) - Add SidebarView: new chat button, conversation search, list with swipe actions - Slim HeaderView to text-only (provider + model + star); remove all icon rows - Move status pills (Online, MCP, Synced) to footer right side - Remove version number and shortcut hints from footer - Add resizable InputBar with drag handle (persisted height) and globe/network.slash online toggle - Fix Norwegian menu appearing on English systems (CFBundleLocalizations in Info.plist) - Add View menu (Model Info, History, Stats, Credits, Online Mode toggle ⌘⇧O) - Add ⌘L as alias for Search Conversations (muscle memory for /load users) - Add Check for Updates to Help menu with download URL from Gitea API - Add one-time Intel/Rosetta deprecation warning on first launch - Swift 6: fix self.Self.isoString() call sites in DatabaseService Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -134,15 +134,29 @@ final class DatabaseService: Sendable {
|
||||
nonisolated static let shared = DatabaseService()
|
||||
|
||||
private let dbQueue: DatabaseQueue
|
||||
private let isoFormatter: ISO8601DateFormatter
|
||||
|
||||
// Command history limit - keep most recent 5000 entries
|
||||
private static let maxHistoryEntries = 5000
|
||||
private nonisolated static let maxHistoryEntries = 5000
|
||||
|
||||
// ISO8601DateFormatter is @MainActor in macOS 27. Use Date.ISO8601FormatStyle (value type, Sendable).
|
||||
private nonisolated static let isoStyle = Date.ISO8601FormatStyle(
|
||||
dateSeparator: .dash,
|
||||
dateTimeSeparator: .standard,
|
||||
timeSeparator: .colon,
|
||||
timeZoneSeparator: .colon,
|
||||
includingFractionalSeconds: true,
|
||||
timeZone: .gmt
|
||||
)
|
||||
|
||||
private nonisolated static func isoString(from date: Date) -> String {
|
||||
isoStyle.format(date)
|
||||
}
|
||||
|
||||
private nonisolated static func isoDate(from string: String) -> Date? {
|
||||
(try? isoStyle.parse(string)) ?? (try? Date(string, strategy: .iso8601))
|
||||
}
|
||||
|
||||
nonisolated private init() {
|
||||
isoFormatter = ISO8601DateFormatter()
|
||||
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
let dbDirectory = appSupport.appendingPathComponent("oAI", isDirectory: true)
|
||||
@@ -156,7 +170,7 @@ final class DatabaseService: Sendable {
|
||||
try! migrator.migrate(dbQueue)
|
||||
}
|
||||
|
||||
private var migrator: DatabaseMigrator {
|
||||
private nonisolated var migrator: DatabaseMigrator {
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
migrator.registerMigration("v1") { db in
|
||||
@@ -375,7 +389,7 @@ final class DatabaseService: Sendable {
|
||||
nonisolated func saveConversation(id: UUID, name: String, messages: [Message], primaryModel: String?) throws -> Conversation {
|
||||
Log.db.info("Saving conversation '\(name)' with \(messages.count) messages (primaryModel: \(primaryModel ?? "none"))")
|
||||
let now = Date()
|
||||
let nowString = isoFormatter.string(from: now)
|
||||
let nowString = Self.isoString(from: now)
|
||||
|
||||
let convRecord = ConversationRecord(
|
||||
id: id.uuidString,
|
||||
@@ -394,7 +408,7 @@ final class DatabaseService: Sendable {
|
||||
content: msg.content,
|
||||
tokens: msg.tokens,
|
||||
cost: msg.cost,
|
||||
timestamp: isoFormatter.string(from: msg.timestamp),
|
||||
timestamp: Self.isoString(from: msg.timestamp),
|
||||
sortOrder: index,
|
||||
modelId: msg.modelId
|
||||
)
|
||||
@@ -420,7 +434,7 @@ final class DatabaseService: Sendable {
|
||||
|
||||
/// Update an existing conversation in-place: rename it, replace all its messages.
|
||||
nonisolated func updateConversation(id: UUID, name: String, messages: [Message], primaryModel: String?) throws {
|
||||
let nowString = isoFormatter.string(from: Date())
|
||||
let nowString = Self.isoString(from: Date())
|
||||
|
||||
let messageRecords = messages.enumerated().compactMap { index, msg -> MessageRecord? in
|
||||
guard msg.role != .system else { return nil }
|
||||
@@ -431,7 +445,7 @@ final class DatabaseService: Sendable {
|
||||
content: msg.content,
|
||||
tokens: msg.tokens,
|
||||
cost: msg.cost,
|
||||
timestamp: isoFormatter.string(from: msg.timestamp),
|
||||
timestamp: Self.isoString(from: msg.timestamp),
|
||||
sortOrder: index,
|
||||
modelId: msg.modelId
|
||||
)
|
||||
@@ -466,7 +480,7 @@ final class DatabaseService: Sendable {
|
||||
let messages = messageRecords.compactMap { record -> Message? in
|
||||
guard let msgId = UUID(uuidString: record.id),
|
||||
let role = MessageRole(rawValue: record.role),
|
||||
let timestamp = self.isoFormatter.date(from: record.timestamp)
|
||||
let timestamp = Self.isoDate(from: record.timestamp)
|
||||
else { return nil }
|
||||
|
||||
let starred = (try? MessageMetadataRecord.fetchOne(db, key: record.id))?.user_starred == 1
|
||||
@@ -484,8 +498,8 @@ final class DatabaseService: Sendable {
|
||||
}
|
||||
|
||||
guard let convId = UUID(uuidString: convRecord.id),
|
||||
let createdAt = self.isoFormatter.date(from: convRecord.createdAt),
|
||||
let updatedAt = self.isoFormatter.date(from: convRecord.updatedAt)
|
||||
let createdAt = Self.isoDate(from: convRecord.createdAt),
|
||||
let updatedAt = Self.isoDate(from: convRecord.updatedAt)
|
||||
else { return nil }
|
||||
|
||||
let conversation = Conversation(
|
||||
@@ -509,8 +523,8 @@ final class DatabaseService: Sendable {
|
||||
|
||||
return records.compactMap { record -> Conversation? in
|
||||
guard let id = UUID(uuidString: record.id),
|
||||
let createdAt = self.isoFormatter.date(from: record.createdAt),
|
||||
let updatedAt = self.isoFormatter.date(from: record.updatedAt)
|
||||
let createdAt = Self.isoDate(from: record.createdAt),
|
||||
let updatedAt = Self.isoDate(from: record.updatedAt)
|
||||
else { return nil }
|
||||
|
||||
// Fetch message count without loading all messages
|
||||
@@ -524,7 +538,7 @@ final class DatabaseService: Sendable {
|
||||
.order(Column("sortOrder").desc)
|
||||
.fetchOne(db)
|
||||
|
||||
let lastDate = lastMsg.flatMap { self.isoFormatter.date(from: $0.timestamp) } ?? updatedAt
|
||||
let lastDate = lastMsg.flatMap { Self.isoDate(from: $0.timestamp) } ?? updatedAt
|
||||
|
||||
// Derive primary model: prefer the stored field, fall back to last message's modelId
|
||||
let primaryModel = record.primaryModel ?? lastMsg?.modelId
|
||||
@@ -574,7 +588,7 @@ final class DatabaseService: Sendable {
|
||||
convRecord.name = name
|
||||
}
|
||||
|
||||
convRecord.updatedAt = self.isoFormatter.string(from: Date())
|
||||
convRecord.updatedAt = Self.isoString(from: Date())
|
||||
try convRecord.update(db)
|
||||
|
||||
if let messages = messages {
|
||||
@@ -589,7 +603,7 @@ final class DatabaseService: Sendable {
|
||||
content: msg.content,
|
||||
tokens: msg.tokens,
|
||||
cost: msg.cost,
|
||||
timestamp: self.isoFormatter.string(from: msg.timestamp),
|
||||
timestamp: Self.isoString(from: msg.timestamp),
|
||||
sortOrder: index
|
||||
)
|
||||
}
|
||||
@@ -610,7 +624,7 @@ final class DatabaseService: Sendable {
|
||||
let record = HistoryRecord(
|
||||
id: UUID().uuidString,
|
||||
input: input,
|
||||
timestamp: isoFormatter.string(from: now)
|
||||
timestamp: Self.isoString(from: now)
|
||||
)
|
||||
|
||||
try? dbQueue.write { db in
|
||||
@@ -643,7 +657,7 @@ final class DatabaseService: Sendable {
|
||||
.fetchAll(db)
|
||||
|
||||
return records.compactMap { record in
|
||||
guard let date = isoFormatter.date(from: record.timestamp) else {
|
||||
guard let date = Self.isoDate(from: record.timestamp) else {
|
||||
return nil
|
||||
}
|
||||
return (input: record.input, timestamp: date)
|
||||
@@ -659,7 +673,7 @@ final class DatabaseService: Sendable {
|
||||
.fetchAll(db)
|
||||
|
||||
return records.compactMap { record in
|
||||
guard let date = isoFormatter.date(from: record.timestamp) else {
|
||||
guard let date = Self.isoDate(from: record.timestamp) else {
|
||||
return nil
|
||||
}
|
||||
return (input: record.input, timestamp: date)
|
||||
@@ -672,7 +686,7 @@ final class DatabaseService: Sendable {
|
||||
nonisolated func saveEmailLog(_ log: EmailLog) {
|
||||
let record = EmailLogRecord(
|
||||
id: log.id.uuidString,
|
||||
timestamp: isoFormatter.string(from: log.timestamp),
|
||||
timestamp: Self.isoString(from: log.timestamp),
|
||||
sender: log.sender,
|
||||
subject: log.subject,
|
||||
emailContent: log.emailContent,
|
||||
@@ -698,7 +712,7 @@ final class DatabaseService: Sendable {
|
||||
.fetchAll(db)
|
||||
|
||||
return records.compactMap { record in
|
||||
guard let timestamp = isoFormatter.date(from: record.timestamp),
|
||||
guard let timestamp = Self.isoDate(from: record.timestamp),
|
||||
let status = EmailLogStatus(rawValue: record.status),
|
||||
let id = UUID(uuidString: record.id) else {
|
||||
return nil
|
||||
@@ -805,7 +819,7 @@ final class DatabaseService: Sendable {
|
||||
// MARK: - Embedding Operations
|
||||
|
||||
nonisolated func saveMessageEmbedding(messageId: UUID, embedding: Data, model: String, dimension: Int) throws {
|
||||
let now = isoFormatter.string(from: Date())
|
||||
let now = Self.isoString(from: Date())
|
||||
let record = MessageEmbeddingRecord(
|
||||
message_id: messageId.uuidString,
|
||||
embedding: embedding,
|
||||
@@ -825,7 +839,7 @@ final class DatabaseService: Sendable {
|
||||
}
|
||||
|
||||
nonisolated func saveConversationEmbedding(conversationId: UUID, embedding: Data, model: String, dimension: Int) throws {
|
||||
let now = isoFormatter.string(from: Date())
|
||||
let now = Self.isoString(from: Date())
|
||||
let record = ConversationEmbeddingRecord(
|
||||
conversation_id: conversationId.uuidString,
|
||||
embedding: embedding,
|
||||
@@ -881,7 +895,7 @@ final class DatabaseService: Sendable {
|
||||
return Array(results.prefix(limit))
|
||||
}
|
||||
|
||||
private func deserializeEmbedding(_ data: Data) -> [Float] {
|
||||
private nonisolated func deserializeEmbedding(_ data: Data) -> [Float] {
|
||||
var embedding: [Float] = []
|
||||
embedding.reserveCapacity(data.count / 4)
|
||||
|
||||
@@ -905,7 +919,7 @@ final class DatabaseService: Sendable {
|
||||
model: String?,
|
||||
tokenCount: Int?
|
||||
) throws {
|
||||
let now = isoFormatter.string(from: Date())
|
||||
let now = Self.isoString(from: Date())
|
||||
let record = ConversationSummaryRecord(
|
||||
id: UUID().uuidString,
|
||||
conversation_id: conversationId.uuidString,
|
||||
|
||||
Reference in New Issue
Block a user