1 Commits
Author SHA1 Message Date
runeandClaude Sonnet 5 0af644456e Update commercial licensing contact URL to oai.pm
Consolidates the mac.oai.pm subdomain references (introduced in the
PolyForm Noncommercial relicense) to the root oai.pm domain, across
the LICENSE file, README, and all Swift source file headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:34:04 +02:00
13 changed files with 52 additions and 299 deletions
-29
View File
@@ -1,29 +0,0 @@
# Security Policy
## Supported Versions
Only the latest publicly released version of oAI is supported with security fixes. Please update to the latest version before reporting an issue, and confirm it still reproduces there.
## Reporting a Vulnerability
If you discover a security vulnerability in oAI, please report it privately rather than opening a public GitHub issue.
To report a security concern, use the contact form at **[https://oai.pm/#contact](https://oai.pm/#contact)**.
Please include as much detail as possible:
- A description of the vulnerability and its potential impact
- Steps to reproduce the issue
- The oAI version and macOS version you're using
- Any relevant logs (`~/Library/Logs/oAI.log`), with sensitive data redacted
## Scope
oAI is a native macOS app that stores conversations, settings, and API keys locally (SQLite database and Keychain). Areas of particular interest for security reports include:
- API key handling and Keychain storage
- MCP file access permission checks
- Bash execution approval flow
- Any path that could lead to data exfiltration or unauthorized local file/system access
## Response
Reports submitted through the contact form will be reviewed and acknowledged as soon as possible. Please allow time for a fix to be developed and released before any public disclosure.
+2 -17
View File
@@ -15,22 +15,9 @@
A550A6622F3B72EA00136F2B /* oAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = oAI.app; sourceTree = BUILT_PRODUCTS_DIR; }; A550A6622F3B72EA00136F2B /* oAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = oAI.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* 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 */ /* Begin PBXFileSystemSynchronizedRootGroup section */
A550A6642F3B72EA00136F2B /* oAI */ = { A550A6642F3B72EA00136F2B /* oAI */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
911C4D0E69E11B84C61453DC /* Exceptions for "oAI" folder in "oAI" target */,
);
path = oAI; path = oAI;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -283,7 +270,6 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly; ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "oAI/Info.plist";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 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_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."; INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
@@ -303,7 +289,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2; MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 2.4.2; MARKETING_VERSION = 2.4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI; PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -333,7 +319,6 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly; ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "oAI/Info.plist";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 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_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."; INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
@@ -353,7 +338,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2; MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 2.4.2; MARKETING_VERSION = 2.4.1;
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI; PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -20,11 +20,7 @@
<nav class="toc"> <nav class="toc">
<h2>Contents</h2> <h2>Contents</h2>
<div class="toc-search"> <ul>
<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="#getting-started">Getting Started</a></li>
<li><a href="#providers">AI Providers &amp; API Keys</a></li> <li><a href="#providers">AI Providers &amp; API Keys</a></li>
<li><a href="#models">Selecting Models</a></li> <li><a href="#models">Selecting Models</a></li>
@@ -1530,6 +1526,9 @@ Whenever the user asks you to translate something, translate it to Norwegian Bok
<dt><kbd>⌘,</kbd></dt> <dt><kbd>⌘,</kbd></dt>
<dd>Open Settings</dd> <dd>Open Settings</dd>
<dt><kbd>⌘/</kbd></dt>
<dd>Show in-app Help</dd>
<dt><kbd>⌘?</kbd></dt> <dt><kbd>⌘?</kbd></dt>
<dd>Open this Help (macOS Help)</dd> <dd>Open this Help (macOS Help)</dd>
@@ -1816,37 +1815,5 @@ 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> <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> </footer>
</div> </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> </body>
</html> </html>
@@ -101,32 +101,6 @@ nav.toc h2 {
margin-bottom: 16px; 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 { nav.toc ul {
list-style: none; list-style: none;
} }
-94
View File
@@ -34,15 +34,6 @@ struct BackupManifest: Codable {
let credentials: [String: String]? 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 // MARK: - BackupService
@Observable @Observable
@@ -60,8 +51,6 @@ final class BackupService {
/// URL of the last backup file /// URL of the last backup file
var lastBackupURL: URL? var lastBackupURL: URL?
private var autoBackupTimer: Timer?
// Keys excluded from backup encrypted_ prefix + internal migration flags // Keys excluded from backup encrypted_ prefix + internal migration flags
private static let excludedKeys: Set<String> = [ private static let excludedKeys: Set<String> = [
"encrypted_openrouterAPIKey", "encrypted_openrouterAPIKey",
@@ -82,7 +71,6 @@ final class BackupService {
private init() { private init() {
checkForExistingBackup() checkForExistingBackup()
startAutoBackupTimer()
} }
// MARK: - iCloud Path Resolution // MARK: - iCloud Path Resolution
@@ -188,88 +176,6 @@ final class BackupService {
log.info("Restored \(manifest.settings.count) settings from backup (v\(manifest.version))") 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 // MARK: - Helpers
private func appVersion() -> String { private func appVersion() -> String {
+1 -2
View File
@@ -63,8 +63,7 @@ class ContactsService {
store.requestAccess(for: .contacts) { granted, error in store.requestAccess(for: .contacts) { granted, error in
let after = CNContactStore.authorizationStatus(for: .contacts) let after = CNContactStore.authorizationStatus(for: .contacts)
if let error { if let error {
let nsError = error as NSError Log.mcp.error("ContactsService.requestAccess: error=\(error.localizedDescription); granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
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 { } else {
Log.mcp.info("ContactsService.requestAccess: granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))") Log.mcp.info("ContactsService.requestAccess: granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
} }
+9 -27
View File
@@ -25,11 +25,16 @@ import Foundation
import os import os
import Security import Security
/// Kill switch for the entire Personal Data tools section (Calendar/Reminders/Contacts/ /// Kill switch for the Personal Data tools (Calendar/Reminders/Contacts/Location & Maps).
/// Location & Maps). Hides the UI and forces every `*Enabled` getter to return `false` /// Flip `isHiddenPendingAppleFix` back to `false` once Apple fixes the macOS 27 beta TCC bug
/// regardless of the persisted DB value no code deleted, just inert. /// (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.
enum PersonalDataTools { enum PersonalDataTools {
/// Hides the entire Personal Data section. Flip to `false` once all four services work.
static let isHiddenPendingAppleFix = false 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 @Observable
@@ -515,33 +520,10 @@ 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) { func toggleFavoriteModel(_ id: String) {
var favs = favoriteModelIds var favs = favoriteModelIds
if favs.contains(id) { favs.remove(id) } else { favs.insert(id) } if favs.contains(id) { favs.remove(id) } else { favs.insert(id) }
favoriteModelIds = favs 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 // MARK: - Anytype MCP Settings
@@ -713,7 +695,7 @@ class SettingsService {
} }
var contactsEnabled: Bool { var contactsEnabled: Bool {
get { !PersonalDataTools.isHiddenPendingAppleFix && cache["contactsEnabled"] == "true" } get { !PersonalDataTools.isHiddenPendingAppleFix && !PersonalDataTools.isContactsHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
set { set {
cache["contactsEnabled"] = String(newValue) cache["contactsEnabled"] = String(newValue)
DatabaseService.shared.setSetting(key: "contactsEnabled", value: String(newValue)) DatabaseService.shared.setSetting(key: "contactsEnabled", value: String(newValue))
-1
View File
@@ -90,7 +90,6 @@ You are a helpful AI assistant. Follow these core principles:
- **Be Direct**: Provide concise, relevant answers. No unnecessary preambles. - **Be Direct**: Provide concise, relevant answers. No unnecessary preambles.
- **Show Your Work**: If you use capabilities (tools, web search, etc.), demonstrate what you did. - **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. - **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 ## FORMATTING
+1 -5
View File
@@ -29,8 +29,7 @@ import Darwin // uname, sysctlbyname
struct ContentView: View { struct ContentView: View {
@Environment(ChatViewModel.self) var chatViewModel @Environment(ChatViewModel.self) var chatViewModel
private var updateService = UpdateCheckService.shared private var updateService = UpdateCheckService.shared
@State private var columnVisibility: NavigationSplitViewVisibility = @State private var columnVisibility: NavigationSplitViewVisibility = .all
SettingsService.shared.sidebarVisible ? .all : .detailOnly
@State private var showIntelWarning = false @State private var showIntelWarning = false
var body: some View { var body: some View {
@@ -47,9 +46,6 @@ struct ContentView: View {
) )
} }
.frame(minWidth: 860, minHeight: 560) .frame(minWidth: 860, minHeight: 560)
.onChange(of: columnVisibility) { _, newValue in
SettingsService.shared.sidebarVisible = newValue != .detailOnly
}
#if os(macOS) #if os(macOS)
.onAppear { .onAppear {
NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed } NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed }
+1
View File
@@ -213,6 +213,7 @@ private let keyboardShortcuts: [(key: String, description: String)] = [
("\u{2318}L", "Conversations"), ("\u{2318}L", "Conversations"),
("\u{21E7}\u{2318}S", "Statistics"), ("\u{21E7}\u{2318}S", "Statistics"),
("\u{2318},", "Settings"), ("\u{2318},", "Settings"),
("\u{2318}/", "Help"),
] ]
// MARK: - HelpView // MARK: - HelpView
+25 -41
View File
@@ -125,7 +125,6 @@ You are a helpful AI assistant. Follow these core principles:
- **Be Direct**: Provide concise, relevant answers. No unnecessary preambles. - **Be Direct**: Provide concise, relevant answers. No unnecessary preambles.
- **Show Your Work**: If you use capabilities (tools, web search, etc.), demonstrate what you did. - **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. - **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 ## FORMATTING
@@ -829,6 +828,10 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
externalMCPSection externalMCPSection
// MARK: Personal Data // 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 { if !PersonalDataTools.isHiddenPendingAppleFix {
Divider() Divider()
@@ -839,8 +842,18 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
.foregroundStyle(.teal) .foregroundStyle(.teal)
Text("Personal Data") Text("Personal Data")
.font(.system(size: 18, weight: .semibold)) .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, 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.") 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.")
.font(.system(size: 14)) .font(.system(size: 14))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
@@ -875,14 +888,16 @@ 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 } requestAccess: { remindersAccessState = await EventKitService.shared.requestReminderAccess() ? .granted : EventKitService.shared.reminderAccessState }
) )
rowDivider() rowDivider()
personalDataRow( if !PersonalDataTools.isContactsHiddenPendingAppleFix {
title: "Contacts", personalDataRow(
isEnabled: $settingsService.contactsEnabled, title: "Contacts",
state: contactsAccessState, isEnabled: $settingsService.contactsEnabled,
systemSettingsAnchor: "Privacy_Contacts", state: contactsAccessState,
requestAccess: { contactsAccessState = await ContactsService.shared.requestAccess() ? .granted : ContactsService.shared.accessState } systemSettingsAnchor: "Privacy_Contacts",
) requestAccess: { contactsAccessState = await ContactsService.shared.requestAccess() ? .granted : ContactsService.shared.accessState }
rowDivider() )
rowDivider()
}
personalDataRow( personalDataRow(
title: "Location & Maps", title: "Location & Maps",
isEnabled: $settingsService.locationMapsEnabled, isEnabled: $settingsService.locationMapsEnabled,
@@ -2580,37 +2595,6 @@ 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 // Actions
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
sectionHeader("Actions") sectionHeader("Actions")
+1 -1
View File
@@ -12,7 +12,7 @@
<true/> <true/>
<key>com.apple.security.personal-information.reminders</key> <key>com.apple.security.personal-information.reminders</key>
<true/> <true/>
<key>com.apple.security.personal-information.addressbook</key> <key>com.apple.security.personal-information.contacts</key>
<true/> <true/>
<key>com.apple.security.personal-information.location</key> <key>com.apple.security.personal-information.location</key>
<true/> <true/>
+8 -19
View File
@@ -43,11 +43,6 @@ struct oAIApp: App {
await GitSyncService.shared.syncOnStartup() 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 // Check for updates in the background
UpdateCheckService.shared.checkForUpdates() UpdateCheckService.shared.checkForUpdates()
} }
@@ -61,11 +56,6 @@ struct oAIApp: App {
AboutView() AboutView()
} }
#if os(macOS) #if os(macOS)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
Task {
await BackupService.shared.syncFavoritesOnLaunch()
}
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in
Task { @MainActor in ExternalMCPManager.shared.stopAll() } Task { @MainActor in ExternalMCPManager.shared.stopAll() }
Task { Task {
@@ -126,11 +116,8 @@ struct oAIApp: App {
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty) .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
} }
// Chat menu // View menu
// Named "Chat" (not "View") to avoid colliding with the "View" menu CommandMenu("View") {
// 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 } Button("Select Model") { chatViewModel.showModelSelector = true }
.keyboardShortcut("m", modifiers: .command) .keyboardShortcut("m", modifiers: .command)
@@ -145,6 +132,9 @@ struct oAIApp: App {
Button("Command History") { chatViewModel.showHistory = true } Button("Command History") { chatViewModel.showHistory = true }
.keyboardShortcut("h", modifiers: [.command, .shift]) .keyboardShortcut("h", modifiers: [.command, .shift])
Button("In-App Help") { chatViewModel.showHelp = true }
.keyboardShortcut("/", modifiers: .command)
Button("Credits") { chatViewModel.showCredits = true } Button("Credits") { chatViewModel.showCredits = true }
Divider() Divider()
@@ -171,12 +161,11 @@ struct oAIApp: App {
#if os(macOS) #if os(macOS)
private func openHelp() { 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) { if let helpBookURL = Bundle.main.url(forResource: "oAI.help", withExtension: nil) {
NSWorkspace.shared.open(helpBookURL.appendingPathComponent("Contents/Resources/en.lproj/index.html")) 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 #endif