Merge pull request '2.4.2' (#8) from 2.4.2 into main
Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
@@ -15,9 +15,22 @@
|
||||
A550A6622F3B72EA00136F2B /* oAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = oAI.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
911C4D0E69E11B84C61453DC /* Exceptions for "oAI" folder in "oAI" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = A550A6612F3B72EA00136F2B /* oAI */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
A550A6642F3B72EA00136F2B /* oAI */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
911C4D0E69E11B84C61453DC /* Exceptions for "oAI" folder in "oAI" target */,
|
||||
);
|
||||
path = oAI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -270,6 +283,7 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "oAI/Info.plist";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
||||
@@ -289,7 +303,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MARKETING_VERSION = 2.4.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -319,6 +333,7 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "oAI/Info.plist";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
||||
@@ -338,7 +353,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MARKETING_VERSION = 2.4.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
|
||||
<nav class="toc">
|
||||
<h2>Contents</h2>
|
||||
<ul>
|
||||
<div class="toc-search">
|
||||
<input type="search" id="tocSearch" placeholder="Search help topics…" aria-label="Search help topics">
|
||||
</div>
|
||||
<p id="tocNoResults" class="toc-no-results" hidden>No topics match your search.</p>
|
||||
<ul id="tocList">
|
||||
<li><a href="#getting-started">Getting Started</a></li>
|
||||
<li><a href="#providers">AI Providers & API Keys</a></li>
|
||||
<li><a href="#models">Selecting Models</a></li>
|
||||
@@ -1526,9 +1530,6 @@ Whenever the user asks you to translate something, translate it to Norwegian Bok
|
||||
<dt><kbd>⌘,</kbd></dt>
|
||||
<dd>Open Settings</dd>
|
||||
|
||||
<dt><kbd>⌘/</kbd></dt>
|
||||
<dd>Show in-app Help</dd>
|
||||
|
||||
<dt><kbd>⌘?</kbd></dt>
|
||||
<dd>Open this Help (macOS Help)</dd>
|
||||
|
||||
@@ -1815,5 +1816,37 @@ Whenever the user asks you to translate something, translate it to Norwegian Bok
|
||||
<p>© 2026 oAI - Rune Olsen. For support or feedback, visit <a href="https://gitlab.pm/rune/oai-swift">gitlab.pm</a> or <a href="mailto:support@fubar.pm?subject=oAI Support&body=What can I help you with?">Contact Us</a>.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var searchInput = document.getElementById('tocSearch');
|
||||
var tocList = document.getElementById('tocList');
|
||||
var noResults = document.getElementById('tocNoResults');
|
||||
if (!searchInput || !tocList) return;
|
||||
|
||||
var entries = Array.prototype.map.call(tocList.querySelectorAll('li'), function (li) {
|
||||
var link = li.querySelector('a');
|
||||
var id = link ? link.getAttribute('href').slice(1) : null;
|
||||
var section = id ? document.getElementById(id) : null;
|
||||
return {
|
||||
li: li,
|
||||
text: (section ? section.textContent : li.textContent).toLowerCase()
|
||||
};
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function () {
|
||||
var query = searchInput.value.trim().toLowerCase();
|
||||
var visibleCount = 0;
|
||||
|
||||
entries.forEach(function (entry) {
|
||||
var matches = query === '' || entry.text.indexOf(query) !== -1;
|
||||
entry.li.hidden = !matches;
|
||||
if (matches) visibleCount++;
|
||||
});
|
||||
|
||||
noResults.hidden = visibleCount > 0;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -101,6 +101,32 @@ nav.toc h2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toc-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toc-search input[type="search"] {
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
padding: 10px 14px;
|
||||
color: var(--text-primary);
|
||||
background: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.toc-search input[type="search"]:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.toc-no-results {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
nav.toc ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,15 @@ struct BackupManifest: Codable {
|
||||
let credentials: [String: String]?
|
||||
}
|
||||
|
||||
// MARK: - FavoritesPayload
|
||||
|
||||
/// Small standalone file (separate from the full settings backup) so starring a model
|
||||
/// syncs near-instantly across machines instead of waiting for the next full backup.
|
||||
struct FavoritesPayload: Codable {
|
||||
let updatedAt: String
|
||||
let ids: [String]
|
||||
}
|
||||
|
||||
// MARK: - BackupService
|
||||
|
||||
@Observable
|
||||
@@ -51,6 +60,8 @@ final class BackupService {
|
||||
/// URL of the last backup file
|
||||
var lastBackupURL: URL?
|
||||
|
||||
private var autoBackupTimer: Timer?
|
||||
|
||||
// Keys excluded from backup — encrypted_ prefix + internal migration flags
|
||||
private static let excludedKeys: Set<String> = [
|
||||
"encrypted_openrouterAPIKey",
|
||||
@@ -71,6 +82,7 @@ final class BackupService {
|
||||
|
||||
private init() {
|
||||
checkForExistingBackup()
|
||||
startAutoBackupTimer()
|
||||
}
|
||||
|
||||
// MARK: - iCloud Path Resolution
|
||||
@@ -176,6 +188,88 @@ final class BackupService {
|
||||
log.info("Restored \(manifest.settings.count) settings from backup (v\(manifest.version))")
|
||||
}
|
||||
|
||||
// MARK: - Favorite Models Sync
|
||||
|
||||
private func favoritesFileURL() -> URL {
|
||||
resolveBackupDirectory().appendingPathComponent("oai_favorites.json")
|
||||
}
|
||||
|
||||
/// Write the current local favorites to iCloud Drive. Called whenever a favorite is toggled.
|
||||
func pushFavorites() async {
|
||||
let settings = SettingsService.shared
|
||||
let payload = FavoritesPayload(
|
||||
updatedAt: settings.favoriteModelsUpdatedAt,
|
||||
ids: settings.favoriteModelIds.sorted()
|
||||
)
|
||||
guard let data = try? JSONEncoder().encode(payload) else { return }
|
||||
try? data.write(to: favoritesFileURL(), options: .atomic)
|
||||
log.debug("Pushed \(payload.ids.count) favorite model(s) to iCloud")
|
||||
}
|
||||
|
||||
/// Reconcile local favorites with the iCloud copy using last-write-wins (by timestamp).
|
||||
/// Call on launch (and optionally on app-become-active) to pick up changes from other machines.
|
||||
func syncFavoritesOnLaunch() async {
|
||||
let settings = SettingsService.shared
|
||||
let fileURL = favoritesFileURL()
|
||||
|
||||
guard let data = try? Data(contentsOf: fileURL),
|
||||
let remote = try? JSONDecoder().decode(FavoritesPayload.self, from: data) else {
|
||||
// No remote copy yet — push local state (may be empty, that's fine).
|
||||
await pushFavorites()
|
||||
return
|
||||
}
|
||||
|
||||
let localUpdatedAt = settings.favoriteModelsUpdatedAt
|
||||
let localIds = settings.favoriteModelIds
|
||||
if remote.updatedAt > localUpdatedAt {
|
||||
settings.favoriteModelIds = Set(remote.ids)
|
||||
settings.favoriteModelsUpdatedAt = remote.updatedAt
|
||||
log.info("Applied \(remote.ids.count) favorite model(s) from iCloud (remote was newer)")
|
||||
} else if localUpdatedAt > remote.updatedAt {
|
||||
await pushFavorites()
|
||||
} else if Set(remote.ids) != localIds {
|
||||
// Tied timestamps (both empty is the common case: two machines that already had
|
||||
// favorites before this sync feature existed, neither ever bumped the timestamp).
|
||||
// Union rather than silently dropping one side's favorites.
|
||||
settings.favoriteModelIds = localIds.union(remote.ids)
|
||||
settings.favoriteModelsUpdatedAt = ISO8601DateFormatter().string(from: Date())
|
||||
await pushFavorites()
|
||||
log.info("Merged favorites from iCloud (tied timestamps) — union of \(localIds.count) local + \(remote.ids.count) remote")
|
||||
}
|
||||
// Equal timestamps and identical sets: already in sync, nothing to do.
|
||||
}
|
||||
|
||||
// MARK: - Automatic Backup
|
||||
|
||||
private static let dailyInterval: TimeInterval = 24 * 3600
|
||||
private static let weeklyInterval: TimeInterval = 7 * 24 * 3600
|
||||
|
||||
/// Checked once at launch and hourly thereafter while the app is running.
|
||||
private func startAutoBackupTimer() {
|
||||
Task { await checkAndPerformAutoBackupIfDue() }
|
||||
autoBackupTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { [weak self] _ in
|
||||
Task { await self?.checkAndPerformAutoBackupIfDue() }
|
||||
}
|
||||
}
|
||||
|
||||
func checkAndPerformAutoBackupIfDue() async {
|
||||
let frequency = SettingsService.shared.autoBackupFrequency
|
||||
let interval: TimeInterval
|
||||
switch frequency {
|
||||
case "daily": interval = Self.dailyInterval
|
||||
case "weekly": interval = Self.weeklyInterval
|
||||
default: return // "manual" — user triggers backups by hand
|
||||
}
|
||||
|
||||
checkForExistingBackup()
|
||||
if let last = lastBackupDate, Date().timeIntervalSince(last) < interval {
|
||||
return // not due yet
|
||||
}
|
||||
|
||||
log.info("Automatic backup (\(frequency, privacy: .public)) is due — backing up now")
|
||||
_ = try? await exportSettings()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func appVersion() -> String {
|
||||
|
||||
@@ -63,7 +63,8 @@ class ContactsService {
|
||||
store.requestAccess(for: .contacts) { granted, error in
|
||||
let after = CNContactStore.authorizationStatus(for: .contacts)
|
||||
if let error {
|
||||
Log.mcp.error("ContactsService.requestAccess: error=\(error.localizedDescription); granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
|
||||
let nsError = error as NSError
|
||||
Log.mcp.error("ContactsService.requestAccess: error=\(error.localizedDescription) domain=\(nsError.domain) code=\(nsError.code) userInfo=\(nsError.userInfo); granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
|
||||
} else {
|
||||
Log.mcp.info("ContactsService.requestAccess: granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
|
||||
}
|
||||
|
||||
@@ -25,16 +25,11 @@ import Foundation
|
||||
import os
|
||||
import Security
|
||||
|
||||
/// Kill switch for the Personal Data tools (Calendar/Reminders/Contacts/Location & Maps).
|
||||
/// Flip `isHiddenPendingAppleFix` back to `false` once Apple fixes the macOS 27 beta TCC bug
|
||||
/// (filed with Apple). Each flag hides the relevant UI and forces the `*Enabled` getter to
|
||||
/// return `false` regardless of the persisted DB value — no code deleted, just inert.
|
||||
/// Kill switch for the entire Personal Data tools section (Calendar/Reminders/Contacts/
|
||||
/// Location & Maps). Hides the UI and forces every `*Enabled` getter to return `false`
|
||||
/// regardless of the persisted DB value — no code deleted, just inert.
|
||||
enum PersonalDataTools {
|
||||
/// Hides the entire Personal Data section. Flip to `false` once all four services work.
|
||||
static let isHiddenPendingAppleFix = false
|
||||
/// Hides only the Contacts row. Contacts TCC still broken under hardened runtime on
|
||||
/// macOS 27 beta 2 while Calendar/Reminders/Location are fixed. Flip to `false` once fixed.
|
||||
static let isContactsHiddenPendingAppleFix = true
|
||||
}
|
||||
|
||||
@Observable
|
||||
@@ -520,10 +515,33 @@ class SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
/// ISO8601 timestamp of the last local change to favoriteModelIds — used to
|
||||
/// resolve last-write-wins conflicts when syncing favorites across machines.
|
||||
var favoriteModelsUpdatedAt: String {
|
||||
get { cache["favoriteModelsUpdatedAt"] ?? "" }
|
||||
set {
|
||||
cache["favoriteModelsUpdatedAt"] = newValue
|
||||
DatabaseService.shared.setSetting(key: "favoriteModelsUpdatedAt", value: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
func toggleFavoriteModel(_ id: String) {
|
||||
var favs = favoriteModelIds
|
||||
if favs.contains(id) { favs.remove(id) } else { favs.insert(id) }
|
||||
favoriteModelIds = favs
|
||||
favoriteModelsUpdatedAt = ISO8601DateFormatter().string(from: Date())
|
||||
Task { await BackupService.shared.pushFavorites() }
|
||||
}
|
||||
|
||||
// MARK: - Automatic Backup
|
||||
|
||||
/// "manual" (default), "daily", or "weekly"
|
||||
var autoBackupFrequency: String {
|
||||
get { cache["autoBackupFrequency"] ?? "manual" }
|
||||
set {
|
||||
cache["autoBackupFrequency"] = newValue
|
||||
DatabaseService.shared.setSetting(key: "autoBackupFrequency", value: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Anytype MCP Settings
|
||||
@@ -695,7 +713,7 @@ class SettingsService {
|
||||
}
|
||||
|
||||
var contactsEnabled: Bool {
|
||||
get { !PersonalDataTools.isHiddenPendingAppleFix && !PersonalDataTools.isContactsHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
|
||||
get { !PersonalDataTools.isHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
|
||||
set {
|
||||
cache["contactsEnabled"] = String(newValue)
|
||||
DatabaseService.shared.setSetting(key: "contactsEnabled", value: String(newValue))
|
||||
|
||||
@@ -90,6 +90,7 @@ You are a helpful AI assistant. Follow these core principles:
|
||||
- **Be Direct**: Provide concise, relevant answers. No unnecessary preambles.
|
||||
- **Show Your Work**: If you use capabilities (tools, web search, etc.), demonstrate what you did.
|
||||
- **Complete Tasks Properly**: If you start something, finish it correctly.
|
||||
- **Match the User's Language**: Always reply in the same language the user is writing in, even when the request itself involves another language (e.g. a translation or spelling request) — the target language applies to the content you produce, not to your own reply.
|
||||
|
||||
## FORMATTING
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ import Darwin // uname, sysctlbyname
|
||||
struct ContentView: View {
|
||||
@Environment(ChatViewModel.self) var chatViewModel
|
||||
private var updateService = UpdateCheckService.shared
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility =
|
||||
SettingsService.shared.sidebarVisible ? .all : .detailOnly
|
||||
@State private var showIntelWarning = false
|
||||
|
||||
var body: some View {
|
||||
@@ -46,6 +47,9 @@ struct ContentView: View {
|
||||
)
|
||||
}
|
||||
.frame(minWidth: 860, minHeight: 560)
|
||||
.onChange(of: columnVisibility) { _, newValue in
|
||||
SettingsService.shared.sidebarVisible = newValue != .detailOnly
|
||||
}
|
||||
#if os(macOS)
|
||||
.onAppear {
|
||||
NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed }
|
||||
|
||||
@@ -213,7 +213,6 @@ private let keyboardShortcuts: [(key: String, description: String)] = [
|
||||
("\u{2318}L", "Conversations"),
|
||||
("\u{21E7}\u{2318}S", "Statistics"),
|
||||
("\u{2318},", "Settings"),
|
||||
("\u{2318}/", "Help"),
|
||||
]
|
||||
|
||||
// MARK: - HelpView
|
||||
|
||||
@@ -125,6 +125,7 @@ You are a helpful AI assistant. Follow these core principles:
|
||||
- **Be Direct**: Provide concise, relevant answers. No unnecessary preambles.
|
||||
- **Show Your Work**: If you use capabilities (tools, web search, etc.), demonstrate what you did.
|
||||
- **Complete Tasks Properly**: If you start something, finish it correctly.
|
||||
- **Match the User's Language**: Always reply in the same language the user is writing in, even when the request itself involves another language (e.g. a translation or spelling request) — the target language applies to the content you produce, not to your own reply.
|
||||
|
||||
## FORMATTING
|
||||
|
||||
@@ -828,10 +829,6 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
externalMCPSection
|
||||
|
||||
// MARK: Personal Data
|
||||
// isHiddenPendingAppleFix hides the entire section (macOS 27 beta TCC bug).
|
||||
// isContactsHiddenPendingAppleFix hides just the Contacts row (still broken in beta 2
|
||||
// under hardened runtime while Calendar/Reminders/Location are fixed). Flip each flag
|
||||
// to false once Apple ships a fix.
|
||||
if !PersonalDataTools.isHiddenPendingAppleFix {
|
||||
Divider()
|
||||
|
||||
@@ -842,18 +839,8 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
.foregroundStyle(.teal)
|
||||
Text("Personal Data")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
if PersonalDataTools.isContactsHiddenPendingAppleFix {
|
||||
Text("β")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(.orange)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange.opacity(0.15))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.help("Beta Feature. May change.")
|
||||
}
|
||||
}
|
||||
Text("Let the AI access your Calendar, Reminders, and Location & Maps to answer questions about your schedule and surroundings. Each service is opt-in and uses standard macOS permission prompts. This functionality is in beta and may change.")
|
||||
Text("Let the AI access your Calendar, Reminders, Contacts, and Location & Maps to answer questions about your schedule and surroundings. Each service is opt-in and uses standard macOS permission prompts. This functionality is in beta and may change.")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -888,16 +875,14 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
requestAccess: { remindersAccessState = await EventKitService.shared.requestReminderAccess() ? .granted : EventKitService.shared.reminderAccessState }
|
||||
)
|
||||
rowDivider()
|
||||
if !PersonalDataTools.isContactsHiddenPendingAppleFix {
|
||||
personalDataRow(
|
||||
title: "Contacts",
|
||||
isEnabled: $settingsService.contactsEnabled,
|
||||
state: contactsAccessState,
|
||||
systemSettingsAnchor: "Privacy_Contacts",
|
||||
requestAccess: { contactsAccessState = await ContactsService.shared.requestAccess() ? .granted : ContactsService.shared.accessState }
|
||||
)
|
||||
rowDivider()
|
||||
}
|
||||
personalDataRow(
|
||||
title: "Contacts",
|
||||
isEnabled: $settingsService.contactsEnabled,
|
||||
state: contactsAccessState,
|
||||
systemSettingsAnchor: "Privacy_Contacts",
|
||||
requestAccess: { contactsAccessState = await ContactsService.shared.requestAccess() ? .granted : ContactsService.shared.accessState }
|
||||
)
|
||||
rowDivider()
|
||||
personalDataRow(
|
||||
title: "Location & Maps",
|
||||
isEnabled: $settingsService.locationMapsEnabled,
|
||||
@@ -2595,6 +2580,37 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
}
|
||||
}
|
||||
|
||||
// Automatic Backup
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Automatic Backup")
|
||||
formSection {
|
||||
row("Frequency") {
|
||||
Picker("", selection: $settingsService.autoBackupFrequency) {
|
||||
Text("Off").tag("manual")
|
||||
Text("Daily").tag("daily")
|
||||
Text("Weekly").tag("weekly")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(width: 220)
|
||||
}
|
||||
}
|
||||
Text("When enabled, oAI backs up automatically in the background (checked at launch and hourly while running) — no need to press \"Back Up Now\" yourself.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
// Favorites Sync
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Favorite Models")
|
||||
Text("Starred models sync automatically via the same iCloud Drive folder — star a model on one Mac and it appears starred on your others within a launch or two.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
// Actions
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Actions")
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.reminders</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.contacts</key>
|
||||
<key>com.apple.security.personal-information.addressbook</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
|
||||
+19
-8
@@ -43,6 +43,11 @@ struct oAIApp: App {
|
||||
await GitSyncService.shared.syncOnStartup()
|
||||
}
|
||||
|
||||
// Reconcile starred models with the iCloud copy (cross-machine favorites sync)
|
||||
Task {
|
||||
await BackupService.shared.syncFavoritesOnLaunch()
|
||||
}
|
||||
|
||||
// Check for updates in the background
|
||||
UpdateCheckService.shared.checkForUpdates()
|
||||
}
|
||||
@@ -56,6 +61,11 @@ struct oAIApp: App {
|
||||
AboutView()
|
||||
}
|
||||
#if os(macOS)
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
Task {
|
||||
await BackupService.shared.syncFavoritesOnLaunch()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in
|
||||
Task { @MainActor in ExternalMCPManager.shared.stopAll() }
|
||||
Task {
|
||||
@@ -116,8 +126,11 @@ struct oAIApp: App {
|
||||
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
||||
}
|
||||
|
||||
// ── View menu ─────────────────────────────────────────────────
|
||||
CommandMenu("View") {
|
||||
// ── Chat menu ─────────────────────────────────────────────────
|
||||
// Named "Chat" (not "View") to avoid colliding with the "View" menu
|
||||
// macOS auto-adds for NavigationSplitView (Enter Full Screen, etc.) —
|
||||
// two menus both titled "View" would otherwise appear in the menu bar.
|
||||
CommandMenu("Chat") {
|
||||
Button("Select Model") { chatViewModel.showModelSelector = true }
|
||||
.keyboardShortcut("m", modifiers: .command)
|
||||
|
||||
@@ -132,9 +145,6 @@ struct oAIApp: App {
|
||||
Button("Command History") { chatViewModel.showHistory = true }
|
||||
.keyboardShortcut("h", modifiers: [.command, .shift])
|
||||
|
||||
Button("In-App Help") { chatViewModel.showHelp = true }
|
||||
.keyboardShortcut("/", modifiers: .command)
|
||||
|
||||
Button("Credits") { chatViewModel.showCredits = true }
|
||||
|
||||
Divider()
|
||||
@@ -161,11 +171,12 @@ struct oAIApp: App {
|
||||
|
||||
#if os(macOS)
|
||||
private func openHelp() {
|
||||
// Opens the Help Book's index.html directly in the default browser rather than
|
||||
// through NSHelpManager/Help Viewer — see CLAUDE.md's macOS 27 beta note for why
|
||||
// (Apple's Tips.app replacement for Help Viewer can't resolve anchors on this beta).
|
||||
// Revisit once macOS 27 reaches RC.
|
||||
if let helpBookURL = Bundle.main.url(forResource: "oAI.help", withExtension: nil) {
|
||||
NSWorkspace.shared.open(helpBookURL.appendingPathComponent("Contents/Resources/en.lproj/index.html"))
|
||||
} else {
|
||||
// Fallback to Apple Help if help book not found
|
||||
NSHelpManager.shared.openHelpAnchor("", inBook: Bundle.main.object(forInfoDictionaryKey: "CFBundleHelpBookName") as? String)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user